Enhanced the gene viewer in the View Target module. Bugs Fixed.
If the gene viewer in the View Target module is enhanced, it will make it easier for users to understand where the guides are located within the target genes.
If the bugs are resolved, users will be able to operate the program without experiencing crashes.
Special thanks to David for stress-testing the program last week. The following issues have been resolved:
My next step is to address any bugs that David may encounter during testing. I will focus on stabilizing the modules with outstanding issues and proceed with packaging the app for Windows. Additionally, I will plan the implementation of the Microbiome Analysis module in collaboration with David.
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM --platform=linux/amd64 python:3.11.9-slim
|
| 2 |
+
|
| 3 |
+
# Install system dependencies including X11, Qt dependencies, and required Linux libraries
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
libgl1-mesa-glx \
|
| 6 |
+
libx11-xcb1 \
|
| 7 |
+
libxcb-icccm4 \
|
| 8 |
+
libxcb-image0 \
|
| 9 |
+
libxcb-keysyms1 \
|
| 10 |
+
libxcb-randr0 \
|
| 11 |
+
libxcb-render-util0 \
|
| 12 |
+
libxcb-shape0 \
|
| 13 |
+
libxcb-xfixes0 \
|
| 14 |
+
libxcb-xinerama0 \
|
| 15 |
+
libxkbcommon-x11-0 \
|
| 16 |
+
xvfb \
|
| 17 |
+
libegl1 \
|
| 18 |
+
libopengl0 \
|
| 19 |
+
libxcb-cursor0 \
|
| 20 |
+
qt6-base-dev \
|
| 21 |
+
glibc-source \
|
| 22 |
+
build-essential \
|
| 23 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 24 |
+
|
| 25 |
+
# Set working directory
|
| 26 |
+
WORKDIR /app
|
| 27 |
+
|
| 28 |
+
# Copy the entire application
|
| 29 |
+
COPY . .
|
| 30 |
+
|
| 31 |
+
# Make sure the SeqFinder executable has correct permissions
|
| 32 |
+
RUN chmod +x /app/src/SeqFinder/Casper_Seq_Finder_Lin
|
| 33 |
+
|
| 34 |
+
# Install Python dependencies
|
| 35 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 36 |
+
|
| 37 |
+
# Set environment variables for Qt
|
| 38 |
+
ENV QT_QPA_PLATFORM=xcb
|
| 39 |
+
ENV XDG_RUNTIME_DIR=/tmp/runtime-root
|
| 40 |
+
ENV DISPLAY=:0
|
| 41 |
+
|
| 42 |
+
# Create runtime directory
|
| 43 |
+
RUN mkdir -p /tmp/runtime-root && chmod 0700 /tmp/runtime-root
|
| 44 |
+
|
| 45 |
+
# Command to run the application
|
| 46 |
+
CMD ["python3", "src/main.py"]
|
|
@@ -20,3 +20,78 @@ Thank you for your interest in CASPER. Our packaged releases for Windows 10 and
|
|
| 20 |
CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
|
| 21 |
|
| 22 |
NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
|
| 21 |
|
| 22 |
NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
|
| 23 |
+
|
| 24 |
+
### Docker Installation (macOS)
|
| 25 |
+
|
| 26 |
+
#### Prerequisites
|
| 27 |
+
1. Install Docker Desktop for Mac
|
| 28 |
+
```bash
|
| 29 |
+
brew install --cask docker
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
2. Install XQuartz
|
| 33 |
+
```bash
|
| 34 |
+
brew install --cask xquartz
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
3. Configure XQuartz:
|
| 38 |
+
```bash
|
| 39 |
+
# Start XQuartz
|
| 40 |
+
open -a XQuartz
|
| 41 |
+
|
| 42 |
+
# In XQuartz Preferences → Security:
|
| 43 |
+
# - Check "Allow connections from network clients"
|
| 44 |
+
# - Restart XQuartz after changing settings
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
4. Set up X11 forwarding (run these commands each time before starting the app):
|
| 48 |
+
```bash
|
| 49 |
+
# Start XQuartz if not running
|
| 50 |
+
open -a XQuartz
|
| 51 |
+
|
| 52 |
+
# Get your IP address
|
| 53 |
+
export IP=$(ifconfig en0 | grep inet | awk '$1=="inet" {print $2}')
|
| 54 |
+
|
| 55 |
+
# Set up permissions (use your actual IP)
|
| 56 |
+
xhost + $IP
|
| 57 |
+
|
| 58 |
+
# Clean up any old containers
|
| 59 |
+
docker-compose down
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
5. Run CASPER:
|
| 63 |
+
```bash
|
| 64 |
+
# First time or after making changes:
|
| 65 |
+
docker-compose up --build
|
| 66 |
+
|
| 67 |
+
# Subsequent runs (without rebuilding):
|
| 68 |
+
docker-compose up
|
| 69 |
+
|
| 70 |
+
# Or run in background:
|
| 71 |
+
docker-compose up -d
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
#### Troubleshooting
|
| 75 |
+
- If the app doesn't start:
|
| 76 |
+
```bash
|
| 77 |
+
# Stop all containers
|
| 78 |
+
docker-compose down
|
| 79 |
+
|
| 80 |
+
# Remove old containers and images
|
| 81 |
+
docker system prune -f
|
| 82 |
+
|
| 83 |
+
# Restart XQuartz
|
| 84 |
+
killall Xquartz
|
| 85 |
+
open -a XQuartz
|
| 86 |
+
|
| 87 |
+
# Set up X11 again
|
| 88 |
+
xhost + localhost
|
| 89 |
+
|
| 90 |
+
# Try running again
|
| 91 |
+
docker-compose up --build
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
- If you still have issues:
|
| 95 |
+
- Make sure Docker Desktop is running
|
| 96 |
+
- Try restarting your computer
|
| 97 |
+
- Run `docker-compose logs` to see detailed error messages
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
casperapp:
|
| 5 |
+
platform: linux/amd64
|
| 6 |
+
build: .
|
| 7 |
+
environment:
|
| 8 |
+
- DISPLAY=host.docker.internal:0
|
| 9 |
+
extra_hosts:
|
| 10 |
+
- "host.docker.internal:host-gateway"
|
| 11 |
+
volumes:
|
| 12 |
+
- /tmp/.X11-unix:/tmp/.X11-unix
|
| 13 |
+
- .:/app
|
| 14 |
+
network_mode: "bridge"
|
| 15 |
+
privileged: true
|
|
@@ -1,30 +1,48 @@
|
|
|
|
|
|
|
|
| 1 |
beautifulsoup4==4.12.3
|
| 2 |
biopython==1.84
|
|
|
|
|
|
|
| 3 |
contourpy==1.3.0
|
| 4 |
cycler==0.12.1
|
| 5 |
darkdetect==0.7.1
|
|
|
|
| 6 |
fonttools==4.53.1
|
|
|
|
|
|
|
|
|
|
| 7 |
joblib==1.4.2
|
| 8 |
kiwisolver==1.4.7
|
| 9 |
lxml==5.3.0
|
|
|
|
| 10 |
matplotlib==3.9.2
|
|
|
|
| 11 |
mplcursors==0.5.3
|
| 12 |
numpy==2.1.1
|
| 13 |
packaging==24.1
|
| 14 |
pandas==2.2.2
|
| 15 |
pillow==10.4.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
pyparsing==3.1.4
|
| 17 |
PyQt6==6.7.1
|
| 18 |
PyQt6-Qt6==6.7.2
|
| 19 |
PyQt6_sip==13.8.0
|
| 20 |
pyqtdarktheme==2.1.0
|
|
|
|
| 21 |
python-dateutil==2.9.0.post0
|
| 22 |
python-dotenv==1.0.1
|
| 23 |
pytz==2024.1
|
| 24 |
PyYAML==6.0.2
|
|
|
|
| 25 |
scikit-learn==1.5.2
|
| 26 |
scipy==1.14.1
|
| 27 |
six==1.16.0
|
| 28 |
soupsieve==2.6
|
| 29 |
threadpoolctl==3.5.0
|
|
|
|
| 30 |
tzdata==2024.1
|
|
|
|
|
|
| 1 |
+
altgraph==0.17.4
|
| 2 |
+
astroid==3.3.5
|
| 3 |
beautifulsoup4==4.12.3
|
| 4 |
biopython==1.84
|
| 5 |
+
certifi==2024.8.30
|
| 6 |
+
charset-normalizer==3.4.0
|
| 7 |
contourpy==1.3.0
|
| 8 |
cycler==0.12.1
|
| 9 |
darkdetect==0.7.1
|
| 10 |
+
dill==0.3.9
|
| 11 |
fonttools==4.53.1
|
| 12 |
+
graphviz==0.20.3
|
| 13 |
+
idna==3.10
|
| 14 |
+
isort==5.13.2
|
| 15 |
joblib==1.4.2
|
| 16 |
kiwisolver==1.4.7
|
| 17 |
lxml==5.3.0
|
| 18 |
+
macholib==1.16.3
|
| 19 |
matplotlib==3.9.2
|
| 20 |
+
mccabe==0.7.0
|
| 21 |
mplcursors==0.5.3
|
| 22 |
numpy==2.1.1
|
| 23 |
packaging==24.1
|
| 24 |
pandas==2.2.2
|
| 25 |
pillow==10.4.0
|
| 26 |
+
platformdirs==4.3.6
|
| 27 |
+
pyinstaller==6.11.1
|
| 28 |
+
pyinstaller-hooks-contrib==2024.10
|
| 29 |
+
pylint==3.3.1
|
| 30 |
pyparsing==3.1.4
|
| 31 |
PyQt6==6.7.1
|
| 32 |
PyQt6-Qt6==6.7.2
|
| 33 |
PyQt6_sip==13.8.0
|
| 34 |
pyqtdarktheme==2.1.0
|
| 35 |
+
python-call-graph==2.1.2
|
| 36 |
python-dateutil==2.9.0.post0
|
| 37 |
python-dotenv==1.0.1
|
| 38 |
pytz==2024.1
|
| 39 |
PyYAML==6.0.2
|
| 40 |
+
requests==2.32.3
|
| 41 |
scikit-learn==1.5.2
|
| 42 |
scipy==1.14.1
|
| 43 |
six==1.16.0
|
| 44 |
soupsieve==2.6
|
| 45 |
threadpoolctl==3.5.0
|
| 46 |
+
tomlkit==0.13.2
|
| 47 |
tzdata==2024.1
|
| 48 |
+
urllib3==2.2.3
|
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
block_cipher = None
|
| 2 |
-
|
| 3 |
-
a = Analysis(['src/main.py'],
|
| 4 |
-
pathex=['src'],
|
| 5 |
-
datas=[
|
| 6 |
-
('assets', 'assets'),
|
| 7 |
-
('config', 'config'),
|
| 8 |
-
('logs', 'logs'),
|
| 9 |
-
('src', 'src'),
|
| 10 |
-
('genomeBrowserTemplate.html', '.'),
|
| 11 |
-
],
|
| 12 |
-
hiddenimports=[],
|
| 13 |
-
hookspath=[],
|
| 14 |
-
runtime_hooks=[],
|
| 15 |
-
excludes=[],
|
| 16 |
-
win_no_prefer_redirects=False,
|
| 17 |
-
win_private_assemblies=False,
|
| 18 |
-
cipher=block_cipher,
|
| 19 |
-
noarchive=False)
|
| 20 |
-
|
| 21 |
-
pyz = PYZ(a.pure, a.zipped_data,
|
| 22 |
-
cipher=block_cipher)
|
| 23 |
-
|
| 24 |
-
exe = EXE(pyz,
|
| 25 |
-
a.scripts,
|
| 26 |
-
[],
|
| 27 |
-
exclude_binaries=True,
|
| 28 |
-
name='CASPERapp',
|
| 29 |
-
debug=False,
|
| 30 |
-
bootloader_ignore_signals=False,
|
| 31 |
-
strip=False,
|
| 32 |
-
upx=True,
|
| 33 |
-
console=False,
|
| 34 |
-
disable_windowed_traceback=False,
|
| 35 |
-
target_arch=None,
|
| 36 |
-
codesign_identity=None,
|
| 37 |
-
entitlements_file=None,
|
| 38 |
-
icon='assets/CASPER_icon.icns')
|
| 39 |
-
|
| 40 |
-
coll = COLLECT(exe,
|
| 41 |
-
a.binaries,
|
| 42 |
-
a.zipfiles,
|
| 43 |
-
a.datas,
|
| 44 |
-
strip=False,
|
| 45 |
-
upx=True,
|
| 46 |
-
upx_exclude=[],
|
| 47 |
-
name='CASPERapp')
|
| 48 |
-
|
| 49 |
-
app = BUNDLE(coll,
|
| 50 |
-
name='CASPERapp.app',
|
| 51 |
-
icon='assets/CASPER_icon.icns',
|
| 52 |
-
version='2.0.1',
|
| 53 |
-
bundle_identifier=None)
|
| 54 |
-
|
| 55 |
-
# 1. Have the mac.spec in the app directory
|
| 56 |
-
# 2. pyinstaller mac.spec
|
| 57 |
-
# 3. mkdir -p dist/dmg
|
| 58 |
-
# 4. rm -r dist/dmg/*
|
| 59 |
-
# 5. Manual copy of the app into dist/dmg
|
| 60 |
-
# 6. create-dmg \
|
| 61 |
-
# --volname "CASPERapp" \
|
| 62 |
-
# --window-pos 200 120 \
|
| 63 |
-
# --window-size 600 300 \
|
| 64 |
-
# --icon-size 100 \
|
| 65 |
-
# --icon "CASPERapp.app" 175 120 \
|
| 66 |
-
# --hide-extension "CASPERapp.app" \
|
| 67 |
-
# --app-drop-link 425 120 \
|
| 68 |
-
# "dist/CASPERapp.dmg" \
|
| 69 |
-
# "dist/dmg/"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2,8 +2,8 @@ from models.FindTargetsModel import FindTargetsModel
|
|
| 2 |
from utils.ui import show_error
|
| 3 |
from views.FindTargetsView import FindTargetsView
|
| 4 |
from PyQt6.QtWidgets import QMessageBox
|
| 5 |
-
from
|
| 6 |
-
import
|
| 7 |
|
| 8 |
class FindTargetsController:
|
| 9 |
def __init__(self, global_settings):
|
|
@@ -14,6 +14,7 @@ class FindTargetsController:
|
|
| 14 |
self.endonuclease = None
|
| 15 |
self._input_data = None
|
| 16 |
self._current_annotation_file = None
|
|
|
|
| 17 |
|
| 18 |
# Connect to annotation file changes
|
| 19 |
self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
|
|
@@ -45,13 +46,10 @@ class FindTargetsController:
|
|
| 45 |
def find_targets(self, input_data):
|
| 46 |
"""Process input data and update existing view or create new one"""
|
| 47 |
try:
|
| 48 |
-
start_time = time.time()
|
| 49 |
-
|
| 50 |
-
# Get current annotation file
|
| 51 |
current_annotation = self.global_settings.get_current_annotation_file()
|
| 52 |
input_data['annotation_file'] = current_annotation
|
| 53 |
self._current_annotation_file = current_annotation
|
| 54 |
-
self._input_data = input_data.copy()
|
| 55 |
|
| 56 |
# Process data and update view
|
| 57 |
self._process_input_data(input_data)
|
|
@@ -62,9 +60,6 @@ class FindTargetsController:
|
|
| 62 |
if not existing_tab:
|
| 63 |
main_window.open_new_tab("Find Targets", self)
|
| 64 |
|
| 65 |
-
total_time = time.time() - start_time
|
| 66 |
-
self.global_settings.logger.debug(f"Total time to process find targets: {total_time:.2f} seconds")
|
| 67 |
-
|
| 68 |
except Exception as e:
|
| 69 |
self.global_settings.logger.error(f"Error in find_targets: {str(e)}")
|
| 70 |
raise
|
|
@@ -72,28 +67,16 @@ class FindTargetsController:
|
|
| 72 |
def _process_input_data(self, input_data):
|
| 73 |
"""Process input data and update view"""
|
| 74 |
try:
|
| 75 |
-
start_time = time.time()
|
| 76 |
-
|
| 77 |
self.global_settings.logger.debug(f"FindTargetsController processing input data: {input_data}")
|
| 78 |
self.organism = input_data['organism']
|
| 79 |
self.endonuclease = input_data['endonuclease']
|
| 80 |
|
| 81 |
# Get new results
|
| 82 |
-
search_start = time.time()
|
| 83 |
results = self.model.find_targets(input_data)
|
| 84 |
-
search_time = time.time() - search_start
|
| 85 |
-
self.global_settings.logger.debug(f"Time to search: {search_time:.2f} seconds")
|
| 86 |
self.global_settings.logger.debug(f"Found {len(results) if results else 0} targets")
|
| 87 |
|
| 88 |
-
# Update view with new results
|
| 89 |
-
view_start = time.time()
|
| 90 |
if results:
|
| 91 |
self.view.display_results(results)
|
| 92 |
-
view_time = time.time() - view_start
|
| 93 |
-
self.global_settings.logger.debug(f"Time to update view: {view_time:.2f} seconds")
|
| 94 |
-
|
| 95 |
-
total_time = time.time() - start_time
|
| 96 |
-
self.global_settings.logger.debug(f"Total time to process data: {total_time:.2f} seconds")
|
| 97 |
|
| 98 |
except Exception as e:
|
| 99 |
self.global_settings.logger.error(f"Error processing input data: {str(e)}")
|
|
@@ -110,28 +93,60 @@ class FindTargetsController:
|
|
| 110 |
QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
|
| 111 |
return
|
| 112 |
|
| 113 |
-
#
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
else:
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
except Exception as e:
|
| 134 |
-
self.
|
| 135 |
if self.view:
|
| 136 |
QMessageBox.critical(self.view, "Error", f"An error occurred while viewing targets: {str(e)}")
|
| 137 |
|
|
|
|
| 2 |
from utils.ui import show_error
|
| 3 |
from views.FindTargetsView import FindTargetsView
|
| 4 |
from PyQt6.QtWidgets import QMessageBox
|
| 5 |
+
from views.LoadingDialog import LoadingDialog
|
| 6 |
+
from PyQt6.QtWidgets import QApplication
|
| 7 |
|
| 8 |
class FindTargetsController:
|
| 9 |
def __init__(self, global_settings):
|
|
|
|
| 14 |
self.endonuclease = None
|
| 15 |
self._input_data = None
|
| 16 |
self._current_annotation_file = None
|
| 17 |
+
self.logger = self.global_settings.logger
|
| 18 |
|
| 19 |
# Connect to annotation file changes
|
| 20 |
self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
|
|
|
|
| 46 |
def find_targets(self, input_data):
|
| 47 |
"""Process input data and update existing view or create new one"""
|
| 48 |
try:
|
|
|
|
|
|
|
|
|
|
| 49 |
current_annotation = self.global_settings.get_current_annotation_file()
|
| 50 |
input_data['annotation_file'] = current_annotation
|
| 51 |
self._current_annotation_file = current_annotation
|
| 52 |
+
self._input_data = input_data.copy()
|
| 53 |
|
| 54 |
# Process data and update view
|
| 55 |
self._process_input_data(input_data)
|
|
|
|
| 60 |
if not existing_tab:
|
| 61 |
main_window.open_new_tab("Find Targets", self)
|
| 62 |
|
|
|
|
|
|
|
|
|
|
| 63 |
except Exception as e:
|
| 64 |
self.global_settings.logger.error(f"Error in find_targets: {str(e)}")
|
| 65 |
raise
|
|
|
|
| 67 |
def _process_input_data(self, input_data):
|
| 68 |
"""Process input data and update view"""
|
| 69 |
try:
|
|
|
|
|
|
|
| 70 |
self.global_settings.logger.debug(f"FindTargetsController processing input data: {input_data}")
|
| 71 |
self.organism = input_data['organism']
|
| 72 |
self.endonuclease = input_data['endonuclease']
|
| 73 |
|
| 74 |
# Get new results
|
|
|
|
| 75 |
results = self.model.find_targets(input_data)
|
|
|
|
|
|
|
| 76 |
self.global_settings.logger.debug(f"Found {len(results) if results else 0} targets")
|
| 77 |
|
|
|
|
|
|
|
| 78 |
if results:
|
| 79 |
self.view.display_results(results)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
except Exception as e:
|
| 82 |
self.global_settings.logger.error(f"Error processing input data: {str(e)}")
|
|
|
|
| 93 |
QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
|
| 94 |
return
|
| 95 |
|
| 96 |
+
# Create loading dialog
|
| 97 |
+
loading_dialog = LoadingDialog(self.view)
|
| 98 |
+
loading_dialog.show()
|
| 99 |
+
loading_dialog.set_progress(0)
|
| 100 |
+
QApplication.processEvents()
|
| 101 |
+
|
| 102 |
+
try:
|
| 103 |
+
# Find existing View Targets tab
|
| 104 |
+
main_window = self.global_settings.main_window
|
| 105 |
+
existing_tab = main_window.find_tab_by_title("View Targets")
|
| 106 |
+
|
| 107 |
+
loading_dialog.set_message("Initializing view targets...", 25)
|
| 108 |
+
QApplication.processEvents()
|
| 109 |
+
|
| 110 |
+
if existing_tab:
|
| 111 |
+
view_targets_controller = main_window.tab_widgets['controllers'].get("View Targets")
|
| 112 |
+
if view_targets_controller:
|
| 113 |
+
loading_dialog.set_message("Loading guides...", 50)
|
| 114 |
+
QApplication.processEvents()
|
| 115 |
+
|
| 116 |
+
# Pass the loading dialog to load_guides
|
| 117 |
+
view_targets_controller.load_guides(
|
| 118 |
+
selected_targets,
|
| 119 |
+
self.organism,
|
| 120 |
+
self.endonuclease,
|
| 121 |
+
loading_dialog=loading_dialog
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Switch to the existing tab
|
| 125 |
+
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 126 |
+
else:
|
| 127 |
+
self.logger.error("View Targets controller not found for existing tab")
|
| 128 |
else:
|
| 129 |
+
loading_dialog.set_message("Creating view targets...", 25)
|
| 130 |
+
QApplication.processEvents()
|
| 131 |
+
|
| 132 |
+
view_targets_controller = self.global_settings.get_view_targets_window()
|
| 133 |
+
|
| 134 |
+
# Pass the loading dialog to load_guides
|
| 135 |
+
view_targets_controller.load_guides(
|
| 136 |
+
selected_targets,
|
| 137 |
+
self.organism,
|
| 138 |
+
self.endonuclease,
|
| 139 |
+
loading_dialog=loading_dialog
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
main_window.open_new_tab("View Targets", view_targets_controller)
|
| 143 |
+
|
| 144 |
+
finally:
|
| 145 |
+
loading_dialog.close()
|
| 146 |
+
QApplication.processEvents()
|
| 147 |
|
| 148 |
except Exception as e:
|
| 149 |
+
self.logger.error(f"Error in view_targets: {str(e)}")
|
| 150 |
if self.view:
|
| 151 |
QMessageBox.critical(self.view, "Error", f"An error occurred while viewing targets: {str(e)}")
|
| 152 |
|
|
@@ -1,12 +1,12 @@
|
|
| 1 |
-
import
|
| 2 |
-
from PyQt6 import
|
| 3 |
-
from PyQt6.QtWidgets import QMainWindow, QMessageBox
|
| 4 |
from views.HomeWindowView import HomeWindowView
|
| 5 |
from models.HomeWindowModel import HomeWindowModel
|
| 6 |
-
from utils.ui import show_error
|
| 7 |
-
from PyQt6.QtCore import QObject
|
| 8 |
-
from controllers.FindTargetsController import FindTargetsController
|
| 9 |
from models.DatabaseManager import FileChangeType
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
class HomeWindowController:
|
| 12 |
def __init__(self, global_settings):
|
|
@@ -72,8 +72,6 @@ class HomeWindowController:
|
|
| 72 |
self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi_window)
|
| 73 |
|
| 74 |
# grpStep3
|
| 75 |
-
# self.view.radio_button_feature.clicked.connect(self.toggle_annotation)
|
| 76 |
-
# self.view.radio_button_position.clicked.connect(self.toggle_annotation)
|
| 77 |
self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
|
| 78 |
self.view.radio_button_position.clicked.connect(self.handle_search_type_change)
|
| 79 |
self.view.radio_button_sequence.clicked.connect(self.handle_search_type_change)
|
|
@@ -100,6 +98,13 @@ class HomeWindowController:
|
|
| 100 |
"The sequence given is too small. At least 100 characters are required."
|
| 101 |
)
|
| 102 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
self.open_view_targets(input_data)
|
| 104 |
elif input_data['search_type'] == 'position':
|
| 105 |
self.open_view_targets(input_data)
|
|
@@ -111,44 +116,88 @@ class HomeWindowController:
|
|
| 111 |
|
| 112 |
def open_view_targets(self, input_data):
|
| 113 |
try:
|
| 114 |
-
# Create
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
# Close existing View Targets tab if it exists
|
| 124 |
-
main_window = self.global_settings.main_window
|
| 125 |
-
existing_tab = main_window.find_tab_by_title("View Targets")
|
| 126 |
-
if existing_tab:
|
| 127 |
-
tab_index = main_window.view.tab_widget.indexOf(existing_tab)
|
| 128 |
-
main_window._close_tab(tab_index)
|
| 129 |
-
self.logger.debug("Closed existing View Targets tab")
|
| 130 |
-
|
| 131 |
-
# Create view targets controller
|
| 132 |
-
view_targets_controller = self.global_settings.get_view_targets_window()
|
| 133 |
-
|
| 134 |
-
view_targets_controller.load_guides(
|
| 135 |
-
targets, # Pass the targets directly
|
| 136 |
-
input_data['organism'],
|
| 137 |
-
input_data['endonuclease']
|
| 138 |
-
)
|
| 139 |
|
| 140 |
-
#
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
except Exception as e:
|
| 154 |
self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
|
|
@@ -157,22 +206,38 @@ class HomeWindowController:
|
|
| 157 |
def open_find_targets_module(self):
|
| 158 |
"""Open find targets module for non-position searches"""
|
| 159 |
try:
|
| 160 |
-
#
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
main_window._close_tab(tab_index)
|
| 166 |
-
self.logger.debug("Closed existing Find Targets tab")
|
| 167 |
-
|
| 168 |
-
# Create new find targets controller and load data
|
| 169 |
-
find_targets_controller = self.global_settings.get_find_targets_window()
|
| 170 |
-
input_data = self.view.get_find_targets_input()
|
| 171 |
-
find_targets_controller.find_targets(input_data)
|
| 172 |
-
|
| 173 |
-
# Open new Find Targets tab
|
| 174 |
-
self.global_settings.main_window.open_new_tab("Find Targets", find_targets_controller)
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
except Exception as e:
|
| 177 |
show_error(self.global_settings, "Error in open_find_targets_module() in Home", str(e))
|
| 178 |
|
|
@@ -209,14 +274,29 @@ class HomeWindowController:
|
|
| 209 |
|
| 210 |
def open_multitargeting_analysis_module(self):
|
| 211 |
try:
|
|
|
|
|
|
|
|
|
|
| 212 |
main_window = self.global_settings.main_window
|
| 213 |
existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
if existing_tab:
|
| 215 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 216 |
main_window._resize_for_tab("Multitargeting Analysis")
|
|
|
|
| 217 |
else:
|
|
|
|
| 218 |
multitargeting_controller = self.global_settings.get_multitargeting_window()
|
|
|
|
|
|
|
|
|
|
| 219 |
main_window.open_new_tab("Multitargeting Analysis", multitargeting_controller)
|
|
|
|
|
|
|
|
|
|
| 220 |
except Exception as e:
|
| 221 |
show_error(self.global_settings, "Error in open_multitargeting_analysis_widget() in Home", str(e))
|
| 222 |
|
|
@@ -267,9 +347,20 @@ class HomeWindowController:
|
|
| 267 |
|
| 268 |
def _handle_db_validation_changed(self, is_valid, message):
|
| 269 |
"""Handle database validation state changes"""
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
def _handle_db_state_changed(self, is_valid, message, changes):
|
| 275 |
"""Handle database state changes"""
|
|
@@ -309,18 +400,6 @@ class HomeWindowController:
|
|
| 309 |
"""Handle changes to the annotation file selection"""
|
| 310 |
self.global_settings.set_current_annotation_file(new_file)
|
| 311 |
|
| 312 |
-
def _update_cspr_related_ui(self):
|
| 313 |
-
# Implementation to update UI elements that depend on CSPR files
|
| 314 |
-
pass
|
| 315 |
-
|
| 316 |
-
def _update_gbff_related_ui(self):
|
| 317 |
-
# Implementation to update UI elements that depend on GBFF files
|
| 318 |
-
pass
|
| 319 |
-
|
| 320 |
-
def _update_validation_state(self, is_valid):
|
| 321 |
-
# Implementation to update UI elements based on validation state
|
| 322 |
-
pass
|
| 323 |
-
|
| 324 |
def handle_search_type_change(self):
|
| 325 |
"""Update UI elements based on search type"""
|
| 326 |
try:
|
|
|
|
| 1 |
+
from PyQt6 import QtWidgets
|
| 2 |
+
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
| 3 |
from views.HomeWindowView import HomeWindowView
|
| 4 |
from models.HomeWindowModel import HomeWindowModel
|
| 5 |
+
from utils.ui import show_error
|
|
|
|
|
|
|
| 6 |
from models.DatabaseManager import FileChangeType
|
| 7 |
+
import time
|
| 8 |
+
from views.LoadingDialog import LoadingDialog
|
| 9 |
+
from PyQt6.QtWidgets import QApplication
|
| 10 |
|
| 11 |
class HomeWindowController:
|
| 12 |
def __init__(self, global_settings):
|
|
|
|
| 72 |
self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi_window)
|
| 73 |
|
| 74 |
# grpStep3
|
|
|
|
|
|
|
| 75 |
self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
|
| 76 |
self.view.radio_button_position.clicked.connect(self.handle_search_type_change)
|
| 77 |
self.view.radio_button_sequence.clicked.connect(self.handle_search_type_change)
|
|
|
|
| 98 |
"The sequence given is too small. At least 100 characters are required."
|
| 99 |
)
|
| 100 |
return
|
| 101 |
+
if len(sequence) > 10000:
|
| 102 |
+
QMessageBox.warning(
|
| 103 |
+
self.view,
|
| 104 |
+
"Sequence Too Long",
|
| 105 |
+
"The sequence given is too large. Maximum allowed length is 10,000 base pairs."
|
| 106 |
+
)
|
| 107 |
+
return
|
| 108 |
self.open_view_targets(input_data)
|
| 109 |
elif input_data['search_type'] == 'position':
|
| 110 |
self.open_view_targets(input_data)
|
|
|
|
| 116 |
|
| 117 |
def open_view_targets(self, input_data):
|
| 118 |
try:
|
| 119 |
+
# Create and show loading dialog
|
| 120 |
+
loading_dialog = LoadingDialog(self.view)
|
| 121 |
+
loading_dialog.show()
|
| 122 |
+
loading_dialog.set_progress(0)
|
| 123 |
+
QApplication.processEvents()
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
# Create find targets controller to use its model
|
| 127 |
+
find_targets_controller = self.global_settings.get_find_targets_window()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
# For position searches, handle each query separately
|
| 130 |
+
if input_data['search_type'] == 'position':
|
| 131 |
+
queries = input_data['search_query'].strip().split('\n')
|
| 132 |
+
all_targets = []
|
| 133 |
+
total_queries = len(queries)
|
| 134 |
+
|
| 135 |
+
for i, query in enumerate(queries):
|
| 136 |
+
# Update loading progress for each query
|
| 137 |
+
progress = int((i / total_queries) * 80) # Leave room for final steps
|
| 138 |
+
loading_dialog.set_message(f"Processing position {i+1} of {total_queries}...", progress)
|
| 139 |
+
QApplication.processEvents()
|
| 140 |
+
|
| 141 |
+
# Create a copy of input data with single query
|
| 142 |
+
query_data = input_data.copy()
|
| 143 |
+
query_data['search_query'] = query.strip()
|
| 144 |
+
|
| 145 |
+
# Get targets for this query
|
| 146 |
+
targets = find_targets_controller.model.find_targets(query_data)
|
| 147 |
+
if targets:
|
| 148 |
+
# Add query information to each target
|
| 149 |
+
for target in targets:
|
| 150 |
+
target['original_query'] = query.strip()
|
| 151 |
+
all_targets.extend(targets)
|
| 152 |
+
|
| 153 |
+
targets = all_targets # Use combined results
|
| 154 |
+
self.logger.debug(f"Processed {len(queries)} queries, found total {len(targets)} targets")
|
| 155 |
+
else:
|
| 156 |
+
# For non-position searches, process normally
|
| 157 |
+
loading_dialog.set_message("Finding targets...", 20)
|
| 158 |
+
QApplication.processEvents()
|
| 159 |
+
targets = find_targets_controller.model.find_targets(input_data)
|
| 160 |
|
| 161 |
+
if targets:
|
| 162 |
+
self.logger.debug(f"Found {len(targets)} targets")
|
| 163 |
+
loading_dialog.set_message("Preparing view targets...", 80)
|
| 164 |
+
QApplication.processEvents()
|
| 165 |
+
|
| 166 |
+
# Close existing View Targets tab if it exists
|
| 167 |
+
main_window = self.global_settings.main_window
|
| 168 |
+
existing_tab = main_window.find_tab_by_title("View Targets")
|
| 169 |
+
if existing_tab:
|
| 170 |
+
tab_index = main_window.view.tab_widget.indexOf(existing_tab)
|
| 171 |
+
main_window._close_tab(tab_index)
|
| 172 |
+
self.logger.debug("Closed existing View Targets tab")
|
| 173 |
+
|
| 174 |
+
# Create view targets controller
|
| 175 |
+
loading_dialog.set_message("Creating view targets...", 90)
|
| 176 |
+
QApplication.processEvents()
|
| 177 |
+
view_targets_controller = self.global_settings.get_view_targets_window()
|
| 178 |
+
|
| 179 |
+
view_targets_controller.load_guides(
|
| 180 |
+
targets,
|
| 181 |
+
input_data['organism'],
|
| 182 |
+
input_data['endonuclease'],
|
| 183 |
+
loading_dialog=loading_dialog
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Open new view targets tab
|
| 187 |
+
main_window.open_new_tab(
|
| 188 |
+
"View Targets",
|
| 189 |
+
view_targets_controller
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
else:
|
| 193 |
+
QtWidgets.QMessageBox.warning(
|
| 194 |
+
self.view,
|
| 195 |
+
"No Targets Found",
|
| 196 |
+
"No targets were found for the specified search."
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
finally:
|
| 200 |
+
loading_dialog.close()
|
| 201 |
|
| 202 |
except Exception as e:
|
| 203 |
self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
|
|
|
|
| 206 |
def open_find_targets_module(self):
|
| 207 |
"""Open find targets module for non-position searches"""
|
| 208 |
try:
|
| 209 |
+
# Show loading dialog
|
| 210 |
+
loading_dialog = LoadingDialog(self.view)
|
| 211 |
+
loading_dialog.show()
|
| 212 |
+
loading_dialog.set_progress(0)
|
| 213 |
+
QApplication.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
+
try:
|
| 216 |
+
# Close existing Find Targets tab if it exists
|
| 217 |
+
main_window = self.global_settings.main_window
|
| 218 |
+
existing_tab = main_window.find_tab_by_title("Find Targets")
|
| 219 |
+
if existing_tab:
|
| 220 |
+
tab_index = main_window.view.tab_widget.indexOf(existing_tab)
|
| 221 |
+
main_window._close_tab(tab_index)
|
| 222 |
+
self.logger.debug("Closed existing Find Targets tab")
|
| 223 |
+
|
| 224 |
+
loading_dialog.set_progress(40)
|
| 225 |
+
|
| 226 |
+
# Create new find targets controller and load data
|
| 227 |
+
find_targets_controller = self.global_settings.get_find_targets_window()
|
| 228 |
+
input_data = self.view.get_find_targets_input()
|
| 229 |
+
loading_dialog.set_progress(60)
|
| 230 |
+
|
| 231 |
+
find_targets_controller.find_targets(input_data)
|
| 232 |
+
loading_dialog.set_progress(80)
|
| 233 |
+
|
| 234 |
+
# Open new Find Targets tab
|
| 235 |
+
self.global_settings.main_window.open_new_tab("Find Targets", find_targets_controller)
|
| 236 |
+
loading_dialog.set_progress(100)
|
| 237 |
+
|
| 238 |
+
finally:
|
| 239 |
+
loading_dialog.close()
|
| 240 |
+
|
| 241 |
except Exception as e:
|
| 242 |
show_error(self.global_settings, "Error in open_find_targets_module() in Home", str(e))
|
| 243 |
|
|
|
|
| 274 |
|
| 275 |
def open_multitargeting_analysis_module(self):
|
| 276 |
try:
|
| 277 |
+
start_time = time.time()
|
| 278 |
+
self.logger.debug("Starting multitargeting analysis module launch")
|
| 279 |
+
|
| 280 |
main_window = self.global_settings.main_window
|
| 281 |
existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
|
| 282 |
+
|
| 283 |
+
tab_check_time = time.time()
|
| 284 |
+
self.logger.debug(f"Tab check took: {tab_check_time - start_time:.2f} seconds")
|
| 285 |
+
|
| 286 |
if existing_tab:
|
| 287 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 288 |
main_window._resize_for_tab("Multitargeting Analysis")
|
| 289 |
+
self.logger.debug(f"Switched to existing tab: {time.time() - tab_check_time:.2f} seconds")
|
| 290 |
else:
|
| 291 |
+
controller_start = time.time()
|
| 292 |
multitargeting_controller = self.global_settings.get_multitargeting_window()
|
| 293 |
+
self.logger.debug(f"Controller creation took: {time.time() - controller_start:.2f} seconds")
|
| 294 |
+
|
| 295 |
+
tab_open_start = time.time()
|
| 296 |
main_window.open_new_tab("Multitargeting Analysis", multitargeting_controller)
|
| 297 |
+
self.logger.debug(f"Tab opening took: {time.time() - tab_open_start:.2f} seconds")
|
| 298 |
+
|
| 299 |
+
self.logger.debug(f"Total multitargeting module launch took: {time.time() - start_time:.2f} seconds")
|
| 300 |
except Exception as e:
|
| 301 |
show_error(self.global_settings, "Error in open_multitargeting_analysis_widget() in Home", str(e))
|
| 302 |
|
|
|
|
| 347 |
|
| 348 |
def _handle_db_validation_changed(self, is_valid, message):
|
| 349 |
"""Handle database validation state changes"""
|
| 350 |
+
try:
|
| 351 |
+
if not is_valid:
|
| 352 |
+
self.view.show_warning("Database Warning", message)
|
| 353 |
+
|
| 354 |
+
# Update UI elements based on validation state
|
| 355 |
+
self.view.push_button_find_view_targets.setEnabled(is_valid)
|
| 356 |
+
self.view.push_button_multitargeting_analysis.setEnabled(is_valid)
|
| 357 |
+
self.view.push_button_population_analysis.setEnabled(is_valid)
|
| 358 |
+
|
| 359 |
+
# Log the validation state change
|
| 360 |
+
self.logger.debug(f"Database validation state changed to: {is_valid}")
|
| 361 |
+
|
| 362 |
+
except Exception as e:
|
| 363 |
+
self.logger.error(f"Error handling database validation change: {str(e)}")
|
| 364 |
|
| 365 |
def _handle_db_state_changed(self, is_valid, message, changes):
|
| 366 |
"""Handle database state changes"""
|
|
|
|
| 400 |
"""Handle changes to the annotation file selection"""
|
| 401 |
self.global_settings.set_current_annotation_file(new_file)
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
def handle_search_type_change(self):
|
| 404 |
"""Update UI elements based on search type"""
|
| 405 |
try:
|
|
@@ -41,11 +41,6 @@ class MainWindowController(LoggingMixin):
|
|
| 41 |
self.view.action_open_NCBI_BLAST.triggered.connect(self._open_ncbi_blast_website)
|
| 42 |
self.view.action_open_NCBI.triggered.connect(self._open_ncbi_website)
|
| 43 |
|
| 44 |
-
# Title Bar
|
| 45 |
-
self.view.close_window_button.clicked.connect(self._close_window)
|
| 46 |
-
self.view.minimize_window_button.clicked.connect(self._minimize_window)
|
| 47 |
-
self.view.maximize_window_button.clicked.connect(self._maximize_window)
|
| 48 |
-
|
| 49 |
# Tab bar
|
| 50 |
self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
|
| 51 |
self.view.tab_widget.tabCloseRequested.connect(self._close_tab)
|
|
@@ -54,8 +49,8 @@ class MainWindowController(LoggingMixin):
|
|
| 54 |
self.settings.first_time_startup.connect(self._handle_first_time_startup)
|
| 55 |
|
| 56 |
# Add Button Menu
|
| 57 |
-
self.view.action_new_genome.triggered.connect(self.open_new_genome_tab)
|
| 58 |
-
self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_tab)
|
| 59 |
|
| 60 |
# Settings Menu
|
| 61 |
self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
|
|
@@ -94,18 +89,23 @@ class MainWindowController(LoggingMixin):
|
|
| 94 |
def _switch_to_home_from_startup(self):
|
| 95 |
self.log_method_call("_switch_to_home_from_startup")
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
startup_tab = self.find_tab_by_title("Startup")
|
| 98 |
if startup_tab:
|
| 99 |
index = self.view.tab_widget.indexOf(startup_tab)
|
| 100 |
self._close_tab(index)
|
| 101 |
-
|
| 102 |
-
if self.startup_controller:
|
| 103 |
-
self.startup_controller.deactivate()
|
| 104 |
-
self.startup_controller = None
|
| 105 |
else:
|
| 106 |
self.log_warning("Startup tab not found when trying to close it")
|
| 107 |
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
| 109 |
self._center_window()
|
| 110 |
|
| 111 |
def _center_window(self):
|
|
@@ -179,18 +179,6 @@ class MainWindowController(LoggingMixin):
|
|
| 179 |
def _open_ncbi_blast_website(self):
|
| 180 |
ncbi_blast_page()
|
| 181 |
|
| 182 |
-
def _close_window(self):
|
| 183 |
-
self.view.close()
|
| 184 |
-
|
| 185 |
-
def _minimize_window(self):
|
| 186 |
-
self.view.showMinimized()
|
| 187 |
-
|
| 188 |
-
def _maximize_window(self):
|
| 189 |
-
if self.view.isMaximized():
|
| 190 |
-
self.view.showNormal()
|
| 191 |
-
else:
|
| 192 |
-
self.view.showMaximized()
|
| 193 |
-
|
| 194 |
def _on_tab_closed(self, widget):
|
| 195 |
"""
|
| 196 |
Handle the tab_closed signal from CloseableTabWidget
|
|
@@ -259,9 +247,8 @@ class MainWindowController(LoggingMixin):
|
|
| 259 |
def _resize_for_tab(self, title):
|
| 260 |
try:
|
| 261 |
if title == "Startup":
|
| 262 |
-
# For Startup tab, set fixed size
|
| 263 |
self.view.setFixedSize(self.startup_size)
|
| 264 |
-
self.view.setWindowFlags(self.view.windowFlags() & ~Qt.WindowType.WindowMaximizeButtonHint)
|
| 265 |
elif title in ["View Targets", "Multitargeting Analysis"]:
|
| 266 |
# Store current size before applying constraints
|
| 267 |
if self.current_tab not in ["View Targets", "Multitargeting Analysis"]:
|
|
@@ -282,12 +269,10 @@ class MainWindowController(LoggingMixin):
|
|
| 282 |
# Set minimum size constraints
|
| 283 |
self.view.setMinimumSize(QSize(min_width, min_height))
|
| 284 |
self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
| 285 |
-
self.view.setWindowFlags(self.view.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
|
| 286 |
else:
|
| 287 |
# For all other tabs
|
| 288 |
self.view.setMinimumSize(QSize(400, 300))
|
| 289 |
self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
| 290 |
-
self.view.setWindowFlags(self.view.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
|
| 291 |
|
| 292 |
# Restore previous size if available and coming from View Targets or Multi-targeting Analysis
|
| 293 |
if self.current_tab in ["View Targets", "Multitargeting Analysis"] and self.previous_size:
|
|
@@ -295,9 +280,6 @@ class MainWindowController(LoggingMixin):
|
|
| 295 |
elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
|
| 296 |
self.view.resize(self.shared_tab_size)
|
| 297 |
|
| 298 |
-
# Ensure window flags are updated
|
| 299 |
-
self.view.show()
|
| 300 |
-
|
| 301 |
# Update the current tab
|
| 302 |
self.current_tab = title
|
| 303 |
|
|
@@ -328,7 +310,7 @@ class MainWindowController(LoggingMixin):
|
|
| 328 |
if title == "New Genome":
|
| 329 |
home_tab = self.find_tab_by_title("Home")
|
| 330 |
if home_tab:
|
| 331 |
-
home_controller = self.
|
| 332 |
home_controller.refresh_data()
|
| 333 |
|
| 334 |
# Resize for the current tab
|
|
@@ -367,7 +349,7 @@ class MainWindowController(LoggingMixin):
|
|
| 367 |
self.view.tab_widget.setCurrentWidget(existing_tab)
|
| 368 |
else:
|
| 369 |
# If it doesn't exist, create a new one
|
| 370 |
-
new_genome_controller = self.
|
| 371 |
new_genome_view = new_genome_controller.view
|
| 372 |
tab_index = self.view.tab_widget.addTab(new_genome_view, "New Genome")
|
| 373 |
self.view.tab_widget.setCurrentIndex(tab_index)
|
|
|
|
| 41 |
self.view.action_open_NCBI_BLAST.triggered.connect(self._open_ncbi_blast_website)
|
| 42 |
self.view.action_open_NCBI.triggered.connect(self._open_ncbi_website)
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
# Tab bar
|
| 45 |
self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
|
| 46 |
self.view.tab_widget.tabCloseRequested.connect(self._close_tab)
|
|
|
|
| 49 |
self.settings.first_time_startup.connect(self._handle_first_time_startup)
|
| 50 |
|
| 51 |
# Add Button Menu
|
| 52 |
+
# self.view.action_new_genome.triggered.connect(self.open_new_genome_tab)
|
| 53 |
+
# self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_tab)
|
| 54 |
|
| 55 |
# Settings Menu
|
| 56 |
self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
|
|
|
|
| 89 |
def _switch_to_home_from_startup(self):
|
| 90 |
self.log_method_call("_switch_to_home_from_startup")
|
| 91 |
|
| 92 |
+
# First deactivate startup controller
|
| 93 |
+
if self.startup_controller:
|
| 94 |
+
self.startup_controller.deactivate()
|
| 95 |
+
self.startup_controller = None
|
| 96 |
+
|
| 97 |
+
# Close startup tab if it exists
|
| 98 |
startup_tab = self.find_tab_by_title("Startup")
|
| 99 |
if startup_tab:
|
| 100 |
index = self.view.tab_widget.indexOf(startup_tab)
|
| 101 |
self._close_tab(index)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
else:
|
| 103 |
self.log_warning("Startup tab not found when trying to close it")
|
| 104 |
|
| 105 |
+
# Open home tab and ensure it's properly initialized
|
| 106 |
+
self._open_home_tab()
|
| 107 |
+
|
| 108 |
+
# Center the window after all tab operations
|
| 109 |
self._center_window()
|
| 110 |
|
| 111 |
def _center_window(self):
|
|
|
|
| 179 |
def _open_ncbi_blast_website(self):
|
| 180 |
ncbi_blast_page()
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
def _on_tab_closed(self, widget):
|
| 183 |
"""
|
| 184 |
Handle the tab_closed signal from CloseableTabWidget
|
|
|
|
| 247 |
def _resize_for_tab(self, title):
|
| 248 |
try:
|
| 249 |
if title == "Startup":
|
| 250 |
+
# For Startup tab, set fixed size but keep window controls
|
| 251 |
self.view.setFixedSize(self.startup_size)
|
|
|
|
| 252 |
elif title in ["View Targets", "Multitargeting Analysis"]:
|
| 253 |
# Store current size before applying constraints
|
| 254 |
if self.current_tab not in ["View Targets", "Multitargeting Analysis"]:
|
|
|
|
| 269 |
# Set minimum size constraints
|
| 270 |
self.view.setMinimumSize(QSize(min_width, min_height))
|
| 271 |
self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
|
|
|
| 272 |
else:
|
| 273 |
# For all other tabs
|
| 274 |
self.view.setMinimumSize(QSize(400, 300))
|
| 275 |
self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
|
|
|
| 276 |
|
| 277 |
# Restore previous size if available and coming from View Targets or Multi-targeting Analysis
|
| 278 |
if self.current_tab in ["View Targets", "Multitargeting Analysis"] and self.previous_size:
|
|
|
|
| 280 |
elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
|
| 281 |
self.view.resize(self.shared_tab_size)
|
| 282 |
|
|
|
|
|
|
|
|
|
|
| 283 |
# Update the current tab
|
| 284 |
self.current_tab = title
|
| 285 |
|
|
|
|
| 310 |
if title == "New Genome":
|
| 311 |
home_tab = self.find_tab_by_title("Home")
|
| 312 |
if home_tab:
|
| 313 |
+
home_controller = self.settings.get_home_window()
|
| 314 |
home_controller.refresh_data()
|
| 315 |
|
| 316 |
# Resize for the current tab
|
|
|
|
| 349 |
self.view.tab_widget.setCurrentWidget(existing_tab)
|
| 350 |
else:
|
| 351 |
# If it doesn't exist, create a new one
|
| 352 |
+
new_genome_controller = self.settings.get_new_genome_window()
|
| 353 |
new_genome_view = new_genome_controller.view
|
| 354 |
tab_index = self.view.tab_widget.addTab(new_genome_view, "New Genome")
|
| 355 |
self.view.tab_widget.setCurrentIndex(tab_index)
|
|
@@ -2,18 +2,17 @@ from PyQt6.QtWidgets import QMainWindow
|
|
| 2 |
from views.MultitargetingWindowView import MultitargetingWindowView
|
| 3 |
from models.MultitargetingWindowModel import MultitargetingWindowModel
|
| 4 |
from utils.ui import show_error, show_message
|
|
|
|
| 5 |
|
| 6 |
class MultitargetingWindowController(QMainWindow):
|
| 7 |
def __init__(self, global_settings):
|
| 8 |
super().__init__()
|
| 9 |
self.settings = global_settings
|
| 10 |
self.logger = global_settings.get_logger()
|
| 11 |
-
|
| 12 |
try:
|
| 13 |
self._model = MultitargetingWindowModel(global_settings)
|
| 14 |
self._view = MultitargetingWindowView(global_settings)
|
| 15 |
self.setCentralWidget(self._view)
|
| 16 |
-
|
| 17 |
self._init_ui()
|
| 18 |
self._setup_connections()
|
| 19 |
except Exception as e:
|
|
@@ -32,33 +31,24 @@ class MultitargetingWindowController(QMainWindow):
|
|
| 32 |
if organisms:
|
| 33 |
self._on_organism_changed(0)
|
| 34 |
|
| 35 |
-
# Initialize plots
|
| 36 |
-
self._view.setup_plots()
|
| 37 |
-
|
| 38 |
-
# Connect max results line edit
|
| 39 |
-
self._view.line_edit_max_results.textChanged.connect(self._on_max_results_changed)
|
| 40 |
-
|
| 41 |
except Exception as e:
|
| 42 |
self.logger.error(f"Error in _init_ui: {str(e)}")
|
| 43 |
show_error(self.settings, "Error", f"Failed to initialize UI: {str(e)}")
|
| 44 |
|
| 45 |
def _setup_connections(self):
|
| 46 |
"""Set up signal-slot connections"""
|
| 47 |
-
# Organism and endonuclease selection
|
| 48 |
self._view.combo_box_organism.currentIndexChanged.connect(self._on_organism_changed)
|
| 49 |
self._view.combo_box_endonuclease.currentIndexChanged.connect(self._on_endonuclease_changed)
|
| 50 |
|
| 51 |
-
# Buttons
|
| 52 |
self._view.push_button_analyze.clicked.connect(self._on_analyze_clicked)
|
| 53 |
-
# self._view.push_button_statistics_overview.clicked.connect(self._on_statistics_overview_clicked)
|
| 54 |
-
# self._view.tool_button_sql_settings.clicked.connect(self._on_sql_settings_clicked)
|
| 55 |
|
| 56 |
-
# Table selection
|
| 57 |
self._view.table_seeds.itemSelectionChanged.connect(self._on_seed_selected)
|
| 58 |
self._view.check_box_select_all.stateChanged.connect(self._on_select_all_changed)
|
| 59 |
|
| 60 |
self._view.push_button_export_selected_gRNAs.clicked.connect(self._handle_export)
|
| 61 |
|
|
|
|
|
|
|
| 62 |
def _on_organism_changed(self, index):
|
| 63 |
"""Handle organism selection change"""
|
| 64 |
try:
|
|
@@ -87,7 +77,14 @@ class MultitargetingWindowController(QMainWindow):
|
|
| 87 |
|
| 88 |
def _on_analyze_clicked(self):
|
| 89 |
"""Handle analyze button click"""
|
|
|
|
| 90 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
organism = self._view.combo_box_organism.currentText()
|
| 92 |
endo = self._view.combo_box_endonuclease.currentText()
|
| 93 |
|
|
@@ -98,6 +95,13 @@ class MultitargetingWindowController(QMainWindow):
|
|
| 98 |
# Load data
|
| 99 |
try:
|
| 100 |
self._model.set_files(organism, endo)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
except FileNotFoundError as e:
|
| 102 |
show_error(self.settings, "File Error",
|
| 103 |
f"Could not find required files for {organism} with {endo}. Please ensure the files exist.")
|
|
@@ -106,33 +110,11 @@ class MultitargetingWindowController(QMainWindow):
|
|
| 106 |
show_error(self.settings, "Input Error", str(e))
|
| 107 |
return
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
# Update UI
|
| 112 |
-
self._view.update_seeds_table(seeds_data)
|
| 113 |
-
self._update_plots()
|
| 114 |
|
| 115 |
except Exception as e:
|
| 116 |
show_error(self.settings, "Analysis Error", str(e))
|
| 117 |
|
| 118 |
-
def _on_statistics_overview_clicked(self):
|
| 119 |
-
"""Handle statistics overview button click"""
|
| 120 |
-
try:
|
| 121 |
-
stats = self._model.calculate_statistics()
|
| 122 |
-
self._show_statistics_dialog(stats)
|
| 123 |
-
except Exception as e:
|
| 124 |
-
show_error(self.settings, "Statistics Error", str(e))
|
| 125 |
-
|
| 126 |
-
def _on_sql_settings_clicked(self):
|
| 127 |
-
"""Handle SQL settings button click"""
|
| 128 |
-
try:
|
| 129 |
-
current_settings = self._model.get_sql_settings()
|
| 130 |
-
if self._show_sql_settings_dialog(current_settings):
|
| 131 |
-
new_settings = self._get_sql_settings_from_dialog()
|
| 132 |
-
self._model.update_sql_settings(new_settings)
|
| 133 |
-
except Exception as e:
|
| 134 |
-
show_error(self.settings, "SQL Settings Error", str(e))
|
| 135 |
-
|
| 136 |
def _on_seed_selected(self):
|
| 137 |
"""Handle seed selection in table"""
|
| 138 |
try:
|
|
@@ -253,32 +235,21 @@ class MultitargetingWindowController(QMainWindow):
|
|
| 253 |
self.logger.error(f"Error in _update_plots: {str(e)}")
|
| 254 |
show_error(self.settings, "Plot Update Error", str(e))
|
| 255 |
|
| 256 |
-
def _show_statistics_dialog(self, stats):
|
| 257 |
-
"""Show statistics overview dialog"""
|
| 258 |
-
# Implement statistics dialog display
|
| 259 |
-
pass
|
| 260 |
-
|
| 261 |
-
def _show_sql_settings_dialog(self, current_settings):
|
| 262 |
-
"""Show SQL settings dialog"""
|
| 263 |
-
# Implement SQL settings dialog display
|
| 264 |
-
return False
|
| 265 |
-
|
| 266 |
-
def _get_sql_settings_from_dialog(self):
|
| 267 |
-
"""Get settings from SQL settings dialog"""
|
| 268 |
-
# Implement getting settings from dialog
|
| 269 |
-
return {}
|
| 270 |
-
|
| 271 |
def _on_max_results_changed(self, value):
|
| 272 |
"""Handle changes to max results setting"""
|
| 273 |
try:
|
| 274 |
-
if value
|
| 275 |
self._model.set_row_limit(1000) # Reset to default
|
| 276 |
return
|
| 277 |
|
| 278 |
-
# Convert to int and
|
| 279 |
limit = int(value)
|
| 280 |
-
if limit <= 0: # Handle negative
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
self._model.set_row_limit(limit)
|
| 283 |
|
| 284 |
except ValueError:
|
|
|
|
| 2 |
from views.MultitargetingWindowView import MultitargetingWindowView
|
| 3 |
from models.MultitargetingWindowModel import MultitargetingWindowModel
|
| 4 |
from utils.ui import show_error, show_message
|
| 5 |
+
import time
|
| 6 |
|
| 7 |
class MultitargetingWindowController(QMainWindow):
|
| 8 |
def __init__(self, global_settings):
|
| 9 |
super().__init__()
|
| 10 |
self.settings = global_settings
|
| 11 |
self.logger = global_settings.get_logger()
|
|
|
|
| 12 |
try:
|
| 13 |
self._model = MultitargetingWindowModel(global_settings)
|
| 14 |
self._view = MultitargetingWindowView(global_settings)
|
| 15 |
self.setCentralWidget(self._view)
|
|
|
|
| 16 |
self._init_ui()
|
| 17 |
self._setup_connections()
|
| 18 |
except Exception as e:
|
|
|
|
| 31 |
if organisms:
|
| 32 |
self._on_organism_changed(0)
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
except Exception as e:
|
| 35 |
self.logger.error(f"Error in _init_ui: {str(e)}")
|
| 36 |
show_error(self.settings, "Error", f"Failed to initialize UI: {str(e)}")
|
| 37 |
|
| 38 |
def _setup_connections(self):
|
| 39 |
"""Set up signal-slot connections"""
|
|
|
|
| 40 |
self._view.combo_box_organism.currentIndexChanged.connect(self._on_organism_changed)
|
| 41 |
self._view.combo_box_endonuclease.currentIndexChanged.connect(self._on_endonuclease_changed)
|
| 42 |
|
|
|
|
| 43 |
self._view.push_button_analyze.clicked.connect(self._on_analyze_clicked)
|
|
|
|
|
|
|
| 44 |
|
|
|
|
| 45 |
self._view.table_seeds.itemSelectionChanged.connect(self._on_seed_selected)
|
| 46 |
self._view.check_box_select_all.stateChanged.connect(self._on_select_all_changed)
|
| 47 |
|
| 48 |
self._view.push_button_export_selected_gRNAs.clicked.connect(self._handle_export)
|
| 49 |
|
| 50 |
+
self._view.line_edit_max_results.textChanged.connect(self._on_max_results_changed)
|
| 51 |
+
|
| 52 |
def _on_organism_changed(self, index):
|
| 53 |
"""Handle organism selection change"""
|
| 54 |
try:
|
|
|
|
| 77 |
|
| 78 |
def _on_analyze_clicked(self):
|
| 79 |
"""Handle analyze button click"""
|
| 80 |
+
analyze_start = time.time()
|
| 81 |
try:
|
| 82 |
+
# Initialize plots if not already done
|
| 83 |
+
if not hasattr(self._view, 'repeats_vs_seed_canvas'):
|
| 84 |
+
plot_init_start = time.time()
|
| 85 |
+
self._view.setup_plots()
|
| 86 |
+
self.logger.debug(f"Plot initialization took: {time.time() - plot_init_start:.2f} seconds")
|
| 87 |
+
|
| 88 |
organism = self._view.combo_box_organism.currentText()
|
| 89 |
endo = self._view.combo_box_endonuclease.currentText()
|
| 90 |
|
|
|
|
| 95 |
# Load data
|
| 96 |
try:
|
| 97 |
self._model.set_files(organism, endo)
|
| 98 |
+
|
| 99 |
+
seeds_data = self._model.get_repeats_data()
|
| 100 |
+
|
| 101 |
+
self._view.update_seeds_table(seeds_data)
|
| 102 |
+
|
| 103 |
+
self._update_plots()
|
| 104 |
+
|
| 105 |
except FileNotFoundError as e:
|
| 106 |
show_error(self.settings, "File Error",
|
| 107 |
f"Could not find required files for {organism} with {endo}. Please ensure the files exist.")
|
|
|
|
| 110 |
show_error(self.settings, "Input Error", str(e))
|
| 111 |
return
|
| 112 |
|
| 113 |
+
self.logger.debug(f"Total analysis took: {time.time() - analyze_start:.2f} seconds")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
except Exception as e:
|
| 116 |
show_error(self.settings, "Analysis Error", str(e))
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
def _on_seed_selected(self):
|
| 119 |
"""Handle seed selection in table"""
|
| 120 |
try:
|
|
|
|
| 235 |
self.logger.error(f"Error in _update_plots: {str(e)}")
|
| 236 |
show_error(self.settings, "Plot Update Error", str(e))
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
def _on_max_results_changed(self, value):
|
| 239 |
"""Handle changes to max results setting"""
|
| 240 |
try:
|
| 241 |
+
if not value: # Handle empty input
|
| 242 |
self._model.set_row_limit(1000) # Reset to default
|
| 243 |
return
|
| 244 |
|
| 245 |
+
# Convert to int and validate
|
| 246 |
limit = int(value)
|
| 247 |
+
if limit <= 0: # Handle negative values
|
| 248 |
+
self._model.set_row_limit(1000) # Reset to default
|
| 249 |
+
self._view.line_edit_max_results.setText("1000")
|
| 250 |
+
return
|
| 251 |
+
|
| 252 |
+
# Update model
|
| 253 |
self._model.set_row_limit(limit)
|
| 254 |
|
| 255 |
except ValueError:
|
|
@@ -32,8 +32,6 @@ class NCBIWindowController:
|
|
| 32 |
self.view.push_button_download_files.clicked.connect(self.download_files_wrapper)
|
| 33 |
self.view.check_box_select_all_rows.clicked.connect(self.select_all_rows_in_table)
|
| 34 |
self.view.radio_button_collections_genbank.toggled.connect(self.is_checked_GenBank_radio_button)
|
| 35 |
-
|
| 36 |
-
self.logger.debug("NCBI Window connections setup completed")
|
| 37 |
except Exception as e:
|
| 38 |
self.logger.error(f"Error setting up connections: {str(e)}", exc_info=True)
|
| 39 |
show_error(self.settings, "Error setting up connections", str(e))
|
|
@@ -48,20 +46,25 @@ class NCBIWindowController:
|
|
| 48 |
self.view.reset_progress()
|
| 49 |
search_params = self.view.get_search_parameters()
|
| 50 |
|
| 51 |
-
# Set
|
|
|
|
|
|
|
|
|
|
| 52 |
if not search_params['organism'].strip():
|
| 53 |
search_params['organism'] = "Escherichia coli"
|
|
|
|
|
|
|
| 54 |
if not search_params['strain'].strip():
|
| 55 |
search_params['strain'] = "K-12"
|
|
|
|
| 56 |
|
| 57 |
-
self.logger.info(f"Querying NCBI with parameters: {search_params}")
|
| 58 |
-
|
| 59 |
self.df = self.model.search_ncbi(search_params)
|
| 60 |
|
| 61 |
if self.df.empty:
|
| 62 |
print("No results found")
|
| 63 |
self.logger.warning("No results found for the given search parameters.")
|
| 64 |
-
show_message(12, QtWidgets.QMessageBox.Icon.Warning, "No Results",
|
|
|
|
| 65 |
else:
|
| 66 |
print(f"Query returned {len(self.df)} results")
|
| 67 |
self.logger.info(f"Query returned {len(self.df)} results")
|
|
@@ -69,7 +72,6 @@ class NCBIWindowController:
|
|
| 69 |
|
| 70 |
self.view.activateWindow()
|
| 71 |
except Exception as e:
|
| 72 |
-
print(f"Error in query_db: {str(e)}")
|
| 73 |
self.logger.error(f"Error in query_db: {str(e)}", exc_info=True)
|
| 74 |
show_error(self.settings, "Error in query_db", e)
|
| 75 |
|
|
@@ -117,7 +119,7 @@ class NCBIWindowController:
|
|
| 117 |
try:
|
| 118 |
self.view.reset_progress()
|
| 119 |
self.total_files = len(selected_rows)
|
| 120 |
-
self.view.progress_bar_download_files.setMaximum(self.total_files)
|
| 121 |
self.view.set_download_files_status_label("Preparing to download...")
|
| 122 |
|
| 123 |
self.model.clear_downloaded_files()
|
|
@@ -131,30 +133,36 @@ class NCBIWindowController:
|
|
| 131 |
strain = self.proxy_model.data(self.proxy_model.index(index.row(), 2))
|
| 132 |
self.logger.info(f"Processing ID: {id}")
|
| 133 |
|
| 134 |
-
|
| 135 |
-
self.logger.info(f"Download
|
| 136 |
-
|
|
|
|
| 137 |
self.logger.warning(f"No download URL found for ID: {id}")
|
| 138 |
self.unavailable_files.append((species_name, strain))
|
| 139 |
self.on_thread_completed() # Increment completed threads for unavailable files
|
| 140 |
continue
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
if not self.download_threads and not self.unavailable_files:
|
| 153 |
show_message(12, QtWidgets.QMessageBox.Icon.Information, "No Downloads", "No valid files selected for download.")
|
| 154 |
return
|
| 155 |
|
| 156 |
except Exception as e:
|
| 157 |
-
self.logger.error(f"Error in download_files: {str(e)}"
|
| 158 |
show_error(self.settings, "Error in download_files", e)
|
| 159 |
|
| 160 |
def on_thread_completed(self):
|
|
|
|
| 32 |
self.view.push_button_download_files.clicked.connect(self.download_files_wrapper)
|
| 33 |
self.view.check_box_select_all_rows.clicked.connect(self.select_all_rows_in_table)
|
| 34 |
self.view.radio_button_collections_genbank.toggled.connect(self.is_checked_GenBank_radio_button)
|
|
|
|
|
|
|
| 35 |
except Exception as e:
|
| 36 |
self.logger.error(f"Error setting up connections: {str(e)}", exc_info=True)
|
| 37 |
show_error(self.settings, "Error setting up connections", str(e))
|
|
|
|
| 46 |
self.view.reset_progress()
|
| 47 |
search_params = self.view.get_search_parameters()
|
| 48 |
|
| 49 |
+
# Set the current database in the model
|
| 50 |
+
self.model.current_database = search_params['database']
|
| 51 |
+
|
| 52 |
+
# Check if organism and strain are provided, set defaults if empty
|
| 53 |
if not search_params['organism'].strip():
|
| 54 |
search_params['organism'] = "Escherichia coli"
|
| 55 |
+
self.view.line_edit_organism.setText(search_params['organism'])
|
| 56 |
+
|
| 57 |
if not search_params['strain'].strip():
|
| 58 |
search_params['strain'] = "K-12"
|
| 59 |
+
self.view.line_edit_strain.setText(search_params['strain'])
|
| 60 |
|
|
|
|
|
|
|
| 61 |
self.df = self.model.search_ncbi(search_params)
|
| 62 |
|
| 63 |
if self.df.empty:
|
| 64 |
print("No results found")
|
| 65 |
self.logger.warning("No results found for the given search parameters.")
|
| 66 |
+
show_message(12, QtWidgets.QMessageBox.Icon.Warning, "No Results",
|
| 67 |
+
"No results found for the given search parameters.")
|
| 68 |
else:
|
| 69 |
print(f"Query returned {len(self.df)} results")
|
| 70 |
self.logger.info(f"Query returned {len(self.df)} results")
|
|
|
|
| 72 |
|
| 73 |
self.view.activateWindow()
|
| 74 |
except Exception as e:
|
|
|
|
| 75 |
self.logger.error(f"Error in query_db: {str(e)}", exc_info=True)
|
| 76 |
show_error(self.settings, "Error in query_db", e)
|
| 77 |
|
|
|
|
| 119 |
try:
|
| 120 |
self.view.reset_progress()
|
| 121 |
self.total_files = len(selected_rows)
|
| 122 |
+
self.view.progress_bar_download_files.setMaximum(self.total_files * 2) # *2 for potential FNA and GBFF files
|
| 123 |
self.view.set_download_files_status_label("Preparing to download...")
|
| 124 |
|
| 125 |
self.model.clear_downloaded_files()
|
|
|
|
| 133 |
strain = self.proxy_model.data(self.proxy_model.index(index.row(), 2))
|
| 134 |
self.logger.info(f"Processing ID: {id}")
|
| 135 |
|
| 136 |
+
urls = self.model.get_download_url(id, self.view.radio_button_collections_genbank.isChecked())
|
| 137 |
+
self.logger.info(f"Download URLs for ID {id}: {urls}")
|
| 138 |
+
|
| 139 |
+
if not urls:
|
| 140 |
self.logger.warning(f"No download URL found for ID: {id}")
|
| 141 |
self.unavailable_files.append((species_name, strain))
|
| 142 |
self.on_thread_completed() # Increment completed threads for unavailable files
|
| 143 |
continue
|
| 144 |
|
| 145 |
+
# Create a download thread for each URL
|
| 146 |
+
for url in urls:
|
| 147 |
+
is_fna = '.fna.' in url.lower()
|
| 148 |
+
downloader = self.model.DownloadThread(
|
| 149 |
+
self, url, id, species_name, strain,
|
| 150 |
+
download_fna=is_fna,
|
| 151 |
+
download_gbff=not is_fna
|
| 152 |
+
)
|
| 153 |
+
downloader.finished.connect(self.on_download_finished)
|
| 154 |
+
downloader.progress_updated.connect(self.update_progress)
|
| 155 |
+
downloader.status_updated.connect(self.update_status)
|
| 156 |
+
downloader.all_completed.connect(self.on_thread_completed)
|
| 157 |
+
self.download_threads.append(downloader)
|
| 158 |
+
downloader.start()
|
| 159 |
|
| 160 |
if not self.download_threads and not self.unavailable_files:
|
| 161 |
show_message(12, QtWidgets.QMessageBox.Icon.Information, "No Downloads", "No valid files selected for download.")
|
| 162 |
return
|
| 163 |
|
| 164 |
except Exception as e:
|
| 165 |
+
self.logger.error(f"Error in download_files: {str(e)}")
|
| 166 |
show_error(self.settings, "Error in download_files", e)
|
| 167 |
|
| 168 |
def on_thread_completed(self):
|
|
@@ -2,6 +2,7 @@ from PyQt6 import QtWidgets, QtCore
|
|
| 2 |
from models.NewGenomeWindowModel import NewGenomeWindowModel
|
| 3 |
from views.NewGenomeWindowView import NewGenomeWindowView
|
| 4 |
from utils.ui import show_message, show_error
|
|
|
|
| 5 |
|
| 6 |
class NewGenomeWindowController:
|
| 7 |
def __init__(self, global_settings):
|
|
@@ -198,9 +199,30 @@ class NewGenomeWindowController:
|
|
| 198 |
program = self.model.get_job_command()
|
| 199 |
command_args = self.model.get_arguments_command_for_job(row_index)
|
| 200 |
self.logger.debug(f"Executing command: {program} {' '.join(command_args)}")
|
|
|
|
| 201 |
|
| 202 |
if self.job_process.state() == QtCore.QProcess.ProcessState.NotRunning:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
self.job_process.start(program, command_args)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
self.logger.debug("Job started")
|
| 205 |
else:
|
| 206 |
self.logger.warning("Process is still running, cannot start a new job.")
|
|
@@ -213,11 +235,27 @@ class NewGenomeWindowController:
|
|
| 213 |
self.view.table_widget_jobs.viewport().update()
|
| 214 |
|
| 215 |
def _handle_job_completion(self, exit_code=None, exit_status=None):
|
| 216 |
-
self.logger.debug("Process finished")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
if hasattr(self, 'job_indexes') and self.job_indexes:
|
| 219 |
completed_row_index = self.job_indexes.pop(0)
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
# Set job as completed
|
| 222 |
self.view.set_job_completed(completed_row_index)
|
| 223 |
|
|
@@ -246,8 +284,26 @@ class NewGenomeWindowController:
|
|
| 246 |
|
| 247 |
def _open_ncbi_module(self):
|
| 248 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
ncbi_controller = self.settings.get_ncbi_window()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
self.settings.main_window.open_new_tab("NCBI Download Tool", ncbi_controller)
|
|
|
|
| 251 |
except Exception as e:
|
| 252 |
show_error(self.settings, "Error opening NCBI module", str(e))
|
| 253 |
self.logger.error(f"Failed to open NCBI module: {str(e)}")
|
|
|
|
| 2 |
from models.NewGenomeWindowModel import NewGenomeWindowModel
|
| 3 |
from views.NewGenomeWindowView import NewGenomeWindowView
|
| 4 |
from utils.ui import show_message, show_error
|
| 5 |
+
import os
|
| 6 |
|
| 7 |
class NewGenomeWindowController:
|
| 8 |
def __init__(self, global_settings):
|
|
|
|
| 199 |
program = self.model.get_job_command()
|
| 200 |
command_args = self.model.get_arguments_command_for_job(row_index)
|
| 201 |
self.logger.debug(f"Executing command: {program} {' '.join(command_args)}")
|
| 202 |
+
self.logger.debug(f"Working directory: {os.getcwd()}")
|
| 203 |
|
| 204 |
if self.job_process.state() == QtCore.QProcess.ProcessState.NotRunning:
|
| 205 |
+
# Set up process output handling
|
| 206 |
+
def handle_stdout():
|
| 207 |
+
output = self.job_process.readAllStandardOutput().data().decode()
|
| 208 |
+
self.logger.debug(f"Process stdout: {output}")
|
| 209 |
+
|
| 210 |
+
def handle_stderr():
|
| 211 |
+
error = self.job_process.readAllStandardError().data().decode()
|
| 212 |
+
self.logger.error(f"Process stderr: {error}")
|
| 213 |
+
|
| 214 |
+
self.job_process.readyReadStandardOutput.connect(handle_stdout)
|
| 215 |
+
self.job_process.readyReadStandardError.connect(handle_stderr)
|
| 216 |
+
|
| 217 |
+
# Start the process
|
| 218 |
self.job_process.start(program, command_args)
|
| 219 |
+
|
| 220 |
+
# Check if process started successfully
|
| 221 |
+
if not self.job_process.waitForStarted(3000): # 3 second timeout
|
| 222 |
+
self.logger.error("Process failed to start")
|
| 223 |
+
self.logger.error(f"Process error: {self.job_process.errorString()}")
|
| 224 |
+
return
|
| 225 |
+
|
| 226 |
self.logger.debug("Job started")
|
| 227 |
else:
|
| 228 |
self.logger.warning("Process is still running, cannot start a new job.")
|
|
|
|
| 235 |
self.view.table_widget_jobs.viewport().update()
|
| 236 |
|
| 237 |
def _handle_job_completion(self, exit_code=None, exit_status=None):
|
| 238 |
+
self.logger.debug(f"Process finished with exit code: {exit_code}")
|
| 239 |
+
|
| 240 |
+
# Log any remaining output
|
| 241 |
+
remaining_output = self.job_process.readAllStandardOutput().data().decode()
|
| 242 |
+
if remaining_output:
|
| 243 |
+
self.logger.debug(f"Final process output: {remaining_output}")
|
| 244 |
+
|
| 245 |
+
remaining_error = self.job_process.readAllStandardError().data().decode()
|
| 246 |
+
if remaining_error:
|
| 247 |
+
self.logger.error(f"Final process error output: {remaining_error}")
|
| 248 |
|
| 249 |
if hasattr(self, 'job_indexes') and self.job_indexes:
|
| 250 |
completed_row_index = self.job_indexes.pop(0)
|
| 251 |
|
| 252 |
+
# Check if output files were created
|
| 253 |
+
expected_cspr_file = os.path.join(self.settings.get_db_path(), f"{self.model.get_job_name(completed_row_index)}.cspr")
|
| 254 |
+
if os.path.exists(expected_cspr_file):
|
| 255 |
+
self.logger.debug(f"CSPR file created successfully: {expected_cspr_file}")
|
| 256 |
+
else:
|
| 257 |
+
self.logger.error(f"Expected CSPR file not found: {expected_cspr_file}")
|
| 258 |
+
|
| 259 |
# Set job as completed
|
| 260 |
self.view.set_job_completed(completed_row_index)
|
| 261 |
|
|
|
|
| 284 |
|
| 285 |
def _open_ncbi_module(self):
|
| 286 |
try:
|
| 287 |
+
# Get organism and strain values from the view
|
| 288 |
+
organism_name = self.view.get_organism_name()
|
| 289 |
+
strain_name = self.view.get_strain()
|
| 290 |
+
|
| 291 |
+
# Get NCBI controller
|
| 292 |
ncbi_controller = self.settings.get_ncbi_window()
|
| 293 |
+
|
| 294 |
+
# Connect to the initialization complete signal
|
| 295 |
+
def on_init_complete():
|
| 296 |
+
if organism_name:
|
| 297 |
+
ncbi_controller.view.line_edit_organism.setText(organism_name)
|
| 298 |
+
if strain_name:
|
| 299 |
+
ncbi_controller.view.line_edit_strain.setText(strain_name)
|
| 300 |
+
|
| 301 |
+
# Connect the signal
|
| 302 |
+
ncbi_controller.view.initialization_complete.connect(on_init_complete)
|
| 303 |
+
|
| 304 |
+
# Open the NCBI tab
|
| 305 |
self.settings.main_window.open_new_tab("NCBI Download Tool", ncbi_controller)
|
| 306 |
+
|
| 307 |
except Exception as e:
|
| 308 |
show_error(self.settings, "Error opening NCBI module", str(e))
|
| 309 |
self.logger.error(f"Failed to open NCBI module: {str(e)}")
|
|
@@ -30,6 +30,7 @@ class StartupWindowController:
|
|
| 30 |
self.view.push_button_go_to_home_or_new_genome.clicked.connect(self._handle_go_to_home_or_new_genome)
|
| 31 |
self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
|
| 32 |
self.model.db_state_updated.connect(self._on_db_state_updated)
|
|
|
|
| 33 |
self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
|
| 34 |
|
| 35 |
def _on_db_path_text_changed(self, new_path):
|
|
@@ -39,6 +40,11 @@ class StartupWindowController:
|
|
| 39 |
if self.is_active and hasattr(self, 'view'):
|
| 40 |
self.view.set_db_status(is_valid, message)
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def _init_ui(self):
|
| 43 |
db_path = self.model.get_db_path()
|
| 44 |
self.logger.debug(f"Initial database path: {db_path}")
|
|
@@ -74,13 +80,11 @@ class StartupWindowController:
|
|
| 74 |
self.open_new_genome_tab()
|
| 75 |
|
| 76 |
def restart_application(self):
|
| 77 |
-
"""Restart the entire application"""
|
| 78 |
try:
|
| 79 |
self.logger.info("Restarting application...")
|
| 80 |
# Get the current application instance
|
| 81 |
app = QtWidgets.QApplication.instance()
|
| 82 |
-
|
| 83 |
-
app.exit(1000) # Changed from QApplication.Exit.ExitCode.Restart
|
| 84 |
except Exception as e:
|
| 85 |
self.logger.error(f"Error restarting application: {str(e)}", exc_info=True)
|
| 86 |
show_error(self.settings, "Error restarting application", str(e))
|
|
@@ -88,7 +92,8 @@ class StartupWindowController:
|
|
| 88 |
def open_new_genome_tab(self):
|
| 89 |
try:
|
| 90 |
self.logger.debug("Opening New Genome tab")
|
| 91 |
-
self.settings
|
|
|
|
| 92 |
except Exception as e:
|
| 93 |
self.logger.error(f"Error opening New Genome tab: {str(e)}", exc_info=True)
|
| 94 |
show_error(self.settings, "Error opening New Genome module", str(e))
|
|
|
|
| 30 |
self.view.push_button_go_to_home_or_new_genome.clicked.connect(self._handle_go_to_home_or_new_genome)
|
| 31 |
self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
|
| 32 |
self.model.db_state_updated.connect(self._on_db_state_updated)
|
| 33 |
+
self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
|
| 34 |
self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
|
| 35 |
|
| 36 |
def _on_db_path_text_changed(self, new_path):
|
|
|
|
| 40 |
if self.is_active and hasattr(self, 'view'):
|
| 41 |
self.view.set_db_status(is_valid, message)
|
| 42 |
|
| 43 |
+
def _on_db_validation_changed(self, is_valid, message):
|
| 44 |
+
"""Handle database validation state changes"""
|
| 45 |
+
if self.is_active and hasattr(self, 'view'):
|
| 46 |
+
self.view.set_db_status(is_valid, message)
|
| 47 |
+
|
| 48 |
def _init_ui(self):
|
| 49 |
db_path = self.model.get_db_path()
|
| 50 |
self.logger.debug(f"Initial database path: {db_path}")
|
|
|
|
| 80 |
self.open_new_genome_tab()
|
| 81 |
|
| 82 |
def restart_application(self):
|
|
|
|
| 83 |
try:
|
| 84 |
self.logger.info("Restarting application...")
|
| 85 |
# Get the current application instance
|
| 86 |
app = QtWidgets.QApplication.instance()
|
| 87 |
+
app.exit(1000)
|
|
|
|
| 88 |
except Exception as e:
|
| 89 |
self.logger.error(f"Error restarting application: {str(e)}", exc_info=True)
|
| 90 |
show_error(self.settings, "Error restarting application", str(e))
|
|
|
|
| 92 |
def open_new_genome_tab(self):
|
| 93 |
try:
|
| 94 |
self.logger.debug("Opening New Genome tab")
|
| 95 |
+
if hasattr(self.settings, 'main_window'):
|
| 96 |
+
self.settings.main_window.open_new_genome_tab()
|
| 97 |
except Exception as e:
|
| 98 |
self.logger.error(f"Error opening New Genome tab: {str(e)}", exc_info=True)
|
| 99 |
show_error(self.settings, "Error opening New Genome module", str(e))
|
|
@@ -1,15 +1,13 @@
|
|
| 1 |
-
import logging
|
| 2 |
from controllers.ScoringOptionsController import ScoringOptionsController
|
| 3 |
from models.ViewTargetsModel import ViewTargetsModel
|
| 4 |
from views.ViewTargetsView import ViewTargetsView
|
| 5 |
from PyQt6.QtWidgets import QMessageBox
|
| 6 |
from utils.ui import show_error
|
| 7 |
-
import
|
| 8 |
-
from PyQt6 import QtWidgets, QtCore
|
| 9 |
import traceback
|
| 10 |
-
import
|
| 11 |
-
from
|
| 12 |
-
import
|
| 13 |
|
| 14 |
class ViewTargetsController:
|
| 15 |
def __init__(self, global_settings):
|
|
@@ -38,75 +36,161 @@ class ViewTargetsController:
|
|
| 38 |
|
| 39 |
self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
|
| 40 |
self.view.spin_box_minimum_on_target_score.valueChanged.connect(self.refresh_guides_display)
|
|
|
|
| 41 |
|
| 42 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
try:
|
| 44 |
self.organism = organism
|
| 45 |
self.endonuclease = endonuclease
|
| 46 |
self.selected_targets = selected_targets
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
self.
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
self.view.combo_box_endonuclease.
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
position_groups[position_name] = target
|
| 76 |
-
formatted_genes.append(position_name)
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
except Exception as e:
|
| 106 |
self.logger.error(f"Error in load_guides: {str(e)}")
|
| 107 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 108 |
show_error(self.settings, "Error loading guides", str(e))
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
def _on_endonuclease_changed(self, new_endonuclease):
|
| 111 |
try:
|
| 112 |
if new_endonuclease != self.endonuclease:
|
|
@@ -142,36 +226,35 @@ class ViewTargetsController:
|
|
| 142 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 143 |
show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
|
| 144 |
|
| 145 |
-
def load_gene_viewer(self):
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
self.logger.debug("No gene selected")
|
| 152 |
-
return
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
if sequence_data:
|
| 162 |
-
# Update gene viewer with sequence
|
| 163 |
-
self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
|
| 176 |
def perform_off_target_analysis(self):
|
| 177 |
"""Launch off-target analysis for selected guides"""
|
|
@@ -216,9 +299,8 @@ class ViewTargetsController:
|
|
| 216 |
def _handle_off_target_results(self, results):
|
| 217 |
"""Handle off-target analysis results"""
|
| 218 |
try:
|
| 219 |
-
scores, details = results
|
| 220 |
|
| 221 |
-
# Get current table headers
|
| 222 |
headers = self.view.get_table_headers()
|
| 223 |
|
| 224 |
# Find Score column index
|
|
@@ -285,44 +367,33 @@ class ViewTargetsController:
|
|
| 285 |
"Please select guides to highlight in the gene viewer.")
|
| 286 |
return
|
| 287 |
|
| 288 |
-
#
|
| 289 |
-
guides_to_highlight = []
|
| 290 |
-
for guide in selected_rows:
|
| 291 |
-
guide_info = {
|
| 292 |
-
'location': guide['location'],
|
| 293 |
-
'sequence': guide['sequence'],
|
| 294 |
-
'strand': guide['strand']
|
| 295 |
-
}
|
| 296 |
-
guides_to_highlight.append(guide_info)
|
| 297 |
-
self.logger.debug(f"Guide to highlight: {guide_info}")
|
| 298 |
-
|
| 299 |
-
# Get current gene sequence
|
| 300 |
current_gene = self.view.combo_box_gene.currentText()
|
|
|
|
| 301 |
|
| 302 |
# Check if this is a position-based search
|
| 303 |
-
if "
|
| 304 |
-
# Parse position from the text
|
| 305 |
try:
|
| 306 |
parts = current_gene.split(',')
|
| 307 |
-
chrom =
|
| 308 |
start = int(parts[1].split('start:')[1].strip())
|
| 309 |
end = int(parts[2].split('end:')[1].strip())
|
| 310 |
|
| 311 |
-
# Get sequence directly
|
| 312 |
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 313 |
-
if
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
'start': start,
|
| 319 |
-
'end': end
|
| 320 |
-
}
|
| 321 |
-
self.logger.debug(f"Got position-based sequence of length: {len(sequence)}")
|
| 322 |
except Exception as e:
|
| 323 |
-
self.logger.error(f"Error
|
| 324 |
-
QMessageBox.warning(
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
| 326 |
return
|
| 327 |
else:
|
| 328 |
# Regular gene-based search
|
|
@@ -336,9 +407,21 @@ class ViewTargetsController:
|
|
| 336 |
QMessageBox.warning(self.view, "No Gene Data",
|
| 337 |
"Could not get gene sequence for highlighting.")
|
| 338 |
return
|
|
|
|
| 339 |
|
| 340 |
-
self.logger.debug(f"
|
| 341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
# Highlight the sequences
|
| 343 |
if guides_to_highlight:
|
| 344 |
self.logger.debug("Attempting to highlight sequences")
|
|
@@ -349,7 +432,8 @@ class ViewTargetsController:
|
|
| 349 |
"Could not get sequence information from the selected rows.")
|
| 350 |
|
| 351 |
except Exception as e:
|
| 352 |
-
self.logger.error(f"Error in highlight_gene_viewer: {str(e)}
|
|
|
|
| 353 |
show_error(self.settings, "Error highlighting gene viewer", str(e))
|
| 354 |
|
| 355 |
def export_targets(self):
|
|
@@ -440,12 +524,13 @@ class ViewTargetsController:
|
|
| 440 |
return
|
| 441 |
|
| 442 |
# Get sequence for new range
|
| 443 |
-
if "
|
| 444 |
# For position-based searches
|
| 445 |
try:
|
| 446 |
parts = current_gene.split(',')
|
| 447 |
-
|
| 448 |
-
|
|
|
|
| 449 |
# Get sequence for new range
|
| 450 |
sequence = self.model._get_sequence_for_position(chrom, new_start, new_end)
|
| 451 |
|
|
@@ -476,6 +561,8 @@ class ViewTargetsController:
|
|
| 476 |
"Could not get gene data for the current selection."
|
| 477 |
)
|
| 478 |
return
|
|
|
|
|
|
|
| 479 |
|
| 480 |
# Get new sequence for the range
|
| 481 |
sequence_data = self.model.get_gene_sequence_for_range(locus_tag, new_start, new_end)
|
|
@@ -504,11 +591,11 @@ class ViewTargetsController:
|
|
| 504 |
current_gene = self.view.combo_box_gene.currentText()
|
| 505 |
|
| 506 |
# Check if this is a position-based search
|
| 507 |
-
if "
|
| 508 |
try:
|
| 509 |
-
# Parse position from the text
|
| 510 |
parts = current_gene.split(',')
|
| 511 |
-
chrom =
|
| 512 |
start = int(parts[1].split('start:')[1].strip())
|
| 513 |
end = int(parts[2].split('end:')[1].strip())
|
| 514 |
|
|
@@ -518,8 +605,8 @@ class ViewTargetsController:
|
|
| 518 |
# Update gene viewer with sequence
|
| 519 |
self.view.set_text_edit_gene_viewer(sequence)
|
| 520 |
|
| 521 |
-
# Update location fields
|
| 522 |
-
self.view.line_edit_start_location.setText(str(start))
|
| 523 |
self.view.line_edit_stop_location.setText(str(end))
|
| 524 |
else:
|
| 525 |
raise ValueError("Could not get sequence for position")
|
|
@@ -541,8 +628,8 @@ class ViewTargetsController:
|
|
| 541 |
# Update gene viewer with sequence
|
| 542 |
self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
|
| 543 |
|
| 544 |
-
# Update location fields
|
| 545 |
-
self.view.line_edit_start_location.setText(str(sequence_data['start']))
|
| 546 |
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 547 |
else:
|
| 548 |
self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
|
|
@@ -596,171 +683,149 @@ class ViewTargetsController:
|
|
| 596 |
def on_gene_selected(self, selected_text):
|
| 597 |
"""Handle gene selection signal"""
|
| 598 |
try:
|
| 599 |
-
|
|
|
|
|
|
|
|
|
|
| 600 |
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
|
|
|
|
|
|
|
|
|
| 605 |
parts = selected_text.split(',')
|
| 606 |
-
chrom =
|
| 607 |
start = int(parts[1].split('start:')[1].strip())
|
| 608 |
end = int(parts[2].split('end:')[1].strip())
|
| 609 |
|
| 610 |
-
# Get sequence directly using _get_sequence_for_position
|
| 611 |
-
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 612 |
-
if sequence:
|
| 613 |
-
# Update gene viewer with sequence
|
| 614 |
-
self.view.set_text_edit_gene_viewer(sequence)
|
| 615 |
-
|
| 616 |
-
# Update location fields
|
| 617 |
-
self.view.line_edit_start_location.setText(str(start))
|
| 618 |
-
self.view.line_edit_stop_location.setText(str(end))
|
| 619 |
-
|
| 620 |
-
self.logger.debug(f"Updated position view with sequence of length: {len(sequence)}")
|
| 621 |
-
|
| 622 |
-
# Filter guides for this position
|
| 623 |
-
position_guides = [g for g in self.model.guides
|
| 624 |
-
if g.get('feature_id') == selected_text]
|
| 625 |
-
self.view.display_guides_in_table(position_guides)
|
| 626 |
-
else:
|
| 627 |
-
self.logger.warning(f"No sequence found for position {chrom}:{start}-{end}")
|
| 628 |
-
self.view.set_text_edit_gene_viewer("No sequence data available for this position")
|
| 629 |
-
self.view.line_edit_start_location.clear()
|
| 630 |
-
self.view.line_edit_stop_location.clear()
|
| 631 |
-
except Exception as e:
|
| 632 |
-
self.logger.error(f"Error handling position selection: {str(e)}")
|
| 633 |
-
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 634 |
-
else:
|
| 635 |
-
# Regular gene-based search
|
| 636 |
-
locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 637 |
-
self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
|
| 638 |
-
|
| 639 |
-
# Get gene sequence with padding using locus tag
|
| 640 |
-
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 641 |
-
if sequence_data:
|
| 642 |
-
# Update gene viewer with sequence
|
| 643 |
-
self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
|
| 644 |
-
|
| 645 |
# Update location fields
|
| 646 |
-
self.view.line_edit_start_location.setText(str(
|
| 647 |
-
self.view.line_edit_stop_location.setText(str(
|
| 648 |
|
| 649 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
|
| 651 |
-
# Filter guides for this gene
|
| 652 |
-
gene_guides = [g for g in self.model.guides
|
| 653 |
-
if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
|
| 654 |
-
self.view.display_guides_in_table(gene_guides)
|
| 655 |
else:
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
self.
|
| 659 |
-
self.view.line_edit_stop_location.clear()
|
| 660 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
except Exception as e:
|
| 662 |
self.logger.error(f"Error handling gene selection: {str(e)}")
|
| 663 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 664 |
|
| 665 |
-
def highlight_guides_in_gene_viewer(self, guides_to_highlight
|
| 666 |
"""Highlight selected guides in gene viewer"""
|
| 667 |
try:
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
guides_to_highlight = self.view.get_selected_guides()
|
| 672 |
-
|
| 673 |
-
self.logger.debug(f"Selected guides: {guides_to_highlight}")
|
| 674 |
-
|
| 675 |
-
if not guides_to_highlight:
|
| 676 |
-
QMessageBox.warning(self.view, "No Selection",
|
| 677 |
-
"Please select guides to highlight in the gene viewer.")
|
| 678 |
-
return
|
| 679 |
-
|
| 680 |
-
# Get current gene sequence
|
| 681 |
-
selected_text = self.view.combo_box_gene.currentText()
|
| 682 |
|
| 683 |
-
#
|
| 684 |
-
if "
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
# Get sequence directly from FindTargetsModel
|
| 693 |
-
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 694 |
-
if not sequence:
|
| 695 |
-
raise ValueError("Could not get sequence for position")
|
| 696 |
-
|
| 697 |
-
self.logger.debug(f"Got sequence of length {len(sequence)} for position-based search")
|
| 698 |
-
|
| 699 |
-
except Exception as e:
|
| 700 |
-
self.logger.error(f"Error parsing position or getting sequence: {str(e)}")
|
| 701 |
-
return
|
| 702 |
else:
|
| 703 |
-
|
| 704 |
-
locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 705 |
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 706 |
-
if not sequence_data or 'sequence' not in sequence_data:
|
| 707 |
-
self.logger.error("No sequence data available for highlighting")
|
| 708 |
-
return
|
| 709 |
-
sequence = sequence_data['sequence']
|
| 710 |
-
|
| 711 |
-
# Process highlights
|
| 712 |
-
highlights = []
|
| 713 |
-
sequences_found = 0
|
| 714 |
-
total_sequences = len(guides_to_highlight)
|
| 715 |
-
|
| 716 |
-
for guide in guides_to_highlight:
|
| 717 |
-
self.logger.debug(f"Processing guide: {guide}")
|
| 718 |
-
sequence_to_find = guide['sequence']
|
| 719 |
-
strand = guide['strand']
|
| 720 |
-
|
| 721 |
-
if strand == '-':
|
| 722 |
-
sequence_to_find = str(Seq(sequence_to_find).reverse_complement())
|
| 723 |
-
self.logger.debug(f"Reverse complemented sequence: {sequence_to_find}")
|
| 724 |
-
|
| 725 |
-
sequence_upper = sequence.upper()
|
| 726 |
-
target_upper = sequence_to_find.upper()
|
| 727 |
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
pos = sequence_upper.find(target_upper)
|
| 731 |
-
if pos != -1:
|
| 732 |
-
self.logger.debug(f"Found sequence at position: {pos}")
|
| 733 |
-
color = 'red' if strand == '-' else 'green'
|
| 734 |
-
highlights.append((pos, len(sequence_to_find), color))
|
| 735 |
-
sequences_found += 1
|
| 736 |
-
else:
|
| 737 |
-
self.logger.debug(f"Sequence not found: {target_upper}")
|
| 738 |
-
|
| 739 |
-
if sequences_found == 0:
|
| 740 |
-
self.logger.warning("No sequences could be highlighted")
|
| 741 |
-
QMessageBox.warning(self.view, "Highlighting Failed",
|
| 742 |
-
"Could not highlight any of the selected sequences in the current gene view.")
|
| 743 |
return
|
| 744 |
-
|
| 745 |
-
# Build highlighted sequence
|
| 746 |
-
result = []
|
| 747 |
-
last_pos = 0
|
| 748 |
-
for pos, length, color in sorted(highlights):
|
| 749 |
-
result.append(sequence[last_pos:pos])
|
| 750 |
-
result.append(f"<span style='background-color: {color};'>")
|
| 751 |
-
result.append(sequence[pos:pos+length])
|
| 752 |
-
result.append("</span>")
|
| 753 |
-
last_pos = pos + length
|
| 754 |
|
| 755 |
-
|
| 756 |
-
|
| 757 |
|
| 758 |
-
#
|
| 759 |
-
self.view.
|
| 760 |
-
self.logger.debug(f"Successfully highlighted {sequences_found} sequences")
|
| 761 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
except Exception as e:
|
| 763 |
-
self.logger.error(f"Error
|
| 764 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 765 |
|
| 766 |
def update_scores(self, scores, algorithm):
|
|
@@ -845,11 +910,11 @@ class ViewTargetsController:
|
|
| 845 |
current_gene = self.view.combo_box_gene.currentText()
|
| 846 |
|
| 847 |
# Reset gene viewer to original sequence
|
| 848 |
-
if "
|
| 849 |
# For position-based searches
|
| 850 |
try:
|
| 851 |
parts = current_gene.split(',')
|
| 852 |
-
chrom =
|
| 853 |
start = int(parts[1].split('start:')[1].strip())
|
| 854 |
end = int(parts[2].split('end:')[1].strip())
|
| 855 |
|
|
@@ -995,3 +1060,45 @@ class ViewTargetsController:
|
|
| 995 |
self.logger.error(f"Error handling co-targeting result: {str(e)}")
|
| 996 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 997 |
show_error(self.settings, "Co-targeting Error", str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from controllers.ScoringOptionsController import ScoringOptionsController
|
| 2 |
from models.ViewTargetsModel import ViewTargetsModel
|
| 3 |
from views.ViewTargetsView import ViewTargetsView
|
| 4 |
from PyQt6.QtWidgets import QMessageBox
|
| 5 |
from utils.ui import show_error
|
| 6 |
+
from PyQt6 import QtWidgets
|
|
|
|
| 7 |
import traceback
|
| 8 |
+
from views.LoadingDialog import LoadingDialog
|
| 9 |
+
from PyQt6.QtWidgets import QApplication
|
| 10 |
+
from PyQt6.QtGui import QColor
|
| 11 |
|
| 12 |
class ViewTargetsController:
|
| 13 |
def __init__(self, global_settings):
|
|
|
|
| 36 |
|
| 37 |
self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
|
| 38 |
self.view.spin_box_minimum_on_target_score.valueChanged.connect(self.refresh_guides_display)
|
| 39 |
+
self.view.check_box_view_exons_only.stateChanged.connect(self._on_view_exons_changed)
|
| 40 |
|
| 41 |
+
def _on_view_exons_changed(self, state):
|
| 42 |
+
"""Handle view exons only checkbox state change"""
|
| 43 |
+
try:
|
| 44 |
+
is_checked = self.view.check_box_view_exons_only.isChecked()
|
| 45 |
+
self.logger.debug(f"View exons only changed to: {is_checked}")
|
| 46 |
+
self.model.set_view_exons_only(is_checked)
|
| 47 |
+
self.refresh_gene_viewer()
|
| 48 |
+
except Exception as e:
|
| 49 |
+
self.logger.error(f"Error handling view exons change: {str(e)}")
|
| 50 |
+
|
| 51 |
+
def load_guides(self, selected_targets, organism, endonuclease, loading_dialog=None):
|
| 52 |
try:
|
| 53 |
self.organism = organism
|
| 54 |
self.endonuclease = endonuclease
|
| 55 |
self.selected_targets = selected_targets
|
| 56 |
|
| 57 |
+
# Use existing loading dialog if provided, otherwise create new one
|
| 58 |
+
using_existing_dialog = loading_dialog is not None
|
| 59 |
+
if not loading_dialog:
|
| 60 |
+
loading_dialog = LoadingDialog(self.view, "Loading guides...")
|
| 61 |
+
loading_dialog.show()
|
| 62 |
+
QApplication.processEvents()
|
| 63 |
|
| 64 |
+
try:
|
| 65 |
+
loading_dialog.set_message("Loading guides...", 60)
|
| 66 |
+
QApplication.processEvents()
|
| 67 |
+
|
| 68 |
+
self.model.load_guides(selected_targets, organism, endonuclease)
|
| 69 |
+
|
| 70 |
+
# Initialize endonuclease combo box
|
| 71 |
+
loading_dialog.set_message("Setting up endonucleases...", 65)
|
| 72 |
+
QApplication.processEvents()
|
| 73 |
|
| 74 |
+
org_to_endo = self.settings.get_organism_to_endonuclease()
|
| 75 |
+
if organism in org_to_endo:
|
| 76 |
+
available_endos = org_to_endo[organism]
|
| 77 |
+
self.view.combo_box_endonuclease.clear()
|
| 78 |
+
self.view.combo_box_endonuclease.addItems(available_endos)
|
| 79 |
|
| 80 |
+
# Set current endonuclease
|
| 81 |
+
current_index = self.view.combo_box_endonuclease.findText(endonuclease)
|
| 82 |
+
if current_index >= 0:
|
| 83 |
+
self.view.combo_box_endonuclease.setCurrentIndex(current_index)
|
| 84 |
+
|
| 85 |
+
self.view.combo_box_endonuclease.currentTextChanged.connect(self._on_endonuclease_changed)
|
| 86 |
+
|
| 87 |
+
loading_dialog.set_message("Processing guides...", 70)
|
| 88 |
+
QApplication.processEvents()
|
| 89 |
+
|
| 90 |
+
guides = self.model.get_guides()
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
loading_dialog.set_message("Updating display...", 80)
|
| 93 |
+
QApplication.processEvents()
|
| 94 |
+
|
| 95 |
+
self.view.display_guides_in_table(guides)
|
| 96 |
+
|
| 97 |
+
# Only trigger gene selection if this is the initial load
|
| 98 |
+
if not hasattr(self, '_initial_load_complete'):
|
| 99 |
+
loading_dialog.set_message("Loading initial gene data...", 90)
|
| 100 |
+
QApplication.processEvents()
|
| 101 |
|
| 102 |
+
# Get unique position names or gene IDs
|
| 103 |
+
unique_entries = set()
|
| 104 |
+
for target in selected_targets:
|
| 105 |
+
if 'feature_id' in target:
|
| 106 |
+
# For position-based searches, use the feature_id directly
|
| 107 |
+
if "chromosome" in str(target['feature_id']):
|
| 108 |
+
unique_entries.add(target['feature_id'])
|
| 109 |
+
else:
|
| 110 |
+
# For gene-based searches, get gene data and format with name
|
| 111 |
+
locus_tag = target['feature_id']
|
| 112 |
+
gene_data = self.model.get_gene_data(locus_tag)
|
| 113 |
+
if gene_data and 'info' in gene_data:
|
| 114 |
+
gene_name = gene_data['info'].get('gene_name', '')
|
| 115 |
+
display_text = f"{locus_tag}: {gene_name}" if gene_name else locus_tag
|
| 116 |
+
unique_entries.add(display_text)
|
| 117 |
|
| 118 |
+
# Convert set to list for combo box
|
| 119 |
+
entries = list(unique_entries)
|
| 120 |
+
self.logger.debug(f"Found {len(entries)} unique entries")
|
| 121 |
+
|
| 122 |
+
self.view.set_combo_box_gene(entries)
|
| 123 |
+
|
| 124 |
+
# Set first entry without triggering the selection signal
|
| 125 |
+
if entries:
|
| 126 |
+
self.view.combo_box_gene.blockSignals(True)
|
| 127 |
+
self.view.combo_box_gene.setCurrentIndex(0)
|
| 128 |
+
self.view.combo_box_gene.blockSignals(False)
|
| 129 |
+
self._load_initial_gene_data(entries[0])
|
| 130 |
+
|
| 131 |
+
self._initial_load_complete = True
|
| 132 |
+
|
| 133 |
+
loading_dialog.set_progress(100)
|
| 134 |
+
QApplication.processEvents()
|
| 135 |
+
|
| 136 |
+
finally:
|
| 137 |
+
# Only close the dialog if we created it
|
| 138 |
+
if not using_existing_dialog:
|
| 139 |
+
loading_dialog.close()
|
| 140 |
+
QApplication.processEvents()
|
| 141 |
+
|
| 142 |
except Exception as e:
|
| 143 |
self.logger.error(f"Error in load_guides: {str(e)}")
|
| 144 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 145 |
show_error(self.settings, "Error loading guides", str(e))
|
| 146 |
|
| 147 |
+
def _load_initial_gene_data(self, selected_text):
|
| 148 |
+
"""Load initial gene data without showing loading dialog"""
|
| 149 |
+
try:
|
| 150 |
+
# Similar to on_gene_selected but without loading dialog
|
| 151 |
+
if "chromosome" in selected_text and "start:" in selected_text:
|
| 152 |
+
# Parse position from the text
|
| 153 |
+
parts = selected_text.split(',')
|
| 154 |
+
chrom = parts[0].split('chromosome')[1].strip() # Remove any extra colons
|
| 155 |
+
start = int(parts[1].split('start:')[1].strip())
|
| 156 |
+
end = int(parts[2].split('end:')[1].strip())
|
| 157 |
+
|
| 158 |
+
self.view.line_edit_start_location.setText(str(start))
|
| 159 |
+
self.view.line_edit_stop_location.setText(str(end))
|
| 160 |
+
|
| 161 |
+
# Get sequence directly for position-based search
|
| 162 |
+
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 163 |
+
if sequence:
|
| 164 |
+
# Update gene viewer with sequence
|
| 165 |
+
self.view.update_gene_viewer(sequence, [])
|
| 166 |
+
self.logger.debug(f"Updated gene viewer with sequence of length {len(sequence)}")
|
| 167 |
+
else:
|
| 168 |
+
self.logger.error(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
|
| 169 |
+
|
| 170 |
+
position_guides = [g for g in self.model.guides
|
| 171 |
+
if g.get('feature_id') == selected_text]
|
| 172 |
+
self.view.display_guides_in_table(position_guides)
|
| 173 |
+
else:
|
| 174 |
+
# Regular gene-based search
|
| 175 |
+
locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 176 |
+
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 177 |
+
if sequence_data:
|
| 178 |
+
self.view.line_edit_start_location.setText(str(sequence_data['start']))
|
| 179 |
+
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 180 |
+
|
| 181 |
+
features = self.model.get_features_for_gene(locus_tag)
|
| 182 |
+
|
| 183 |
+
self.view.update_gene_viewer(sequence_data['sequence'], features)
|
| 184 |
+
|
| 185 |
+
gene_guides = [g for g in self.model.guides
|
| 186 |
+
if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
|
| 187 |
+
self.view.display_guides_in_table(gene_guides)
|
| 188 |
+
|
| 189 |
+
self.logger.debug("Initial gene data loaded successfully")
|
| 190 |
+
|
| 191 |
+
except Exception as e:
|
| 192 |
+
self.logger.error(f"Error loading initial gene data: {str(e)}")
|
| 193 |
+
|
| 194 |
def _on_endonuclease_changed(self, new_endonuclease):
|
| 195 |
try:
|
| 196 |
if new_endonuclease != self.endonuclease:
|
|
|
|
| 226 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 227 |
show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
|
| 228 |
|
| 229 |
+
# def load_gene_viewer(self):
|
| 230 |
+
# try:
|
| 231 |
+
# # Get selected gene from combo box
|
| 232 |
+
# selected_text = self.view.combo_box_gene.currentText()
|
| 233 |
+
# if not selected_text:
|
| 234 |
+
# self.logger.debug("No gene selected")
|
| 235 |
+
# return
|
| 236 |
|
| 237 |
+
# # Extract locus tag from "locus_tag: gene_name" format
|
| 238 |
+
# locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 239 |
+
# self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
# # Get gene sequence with padding
|
| 242 |
+
# sequence_data = self.model.get_gene_sequence(locus_tag)
|
|
|
|
| 243 |
|
| 244 |
+
# if sequence_data:
|
| 245 |
+
# # Update gene viewer with sequence
|
| 246 |
+
# self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
+
# # Update location fields
|
| 249 |
+
# self.view.line_edit_start_location.setText(str(sequence_data['start']))
|
| 250 |
+
# self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 251 |
|
| 252 |
+
# else:
|
| 253 |
+
# self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
|
| 254 |
|
| 255 |
+
# except Exception as e:
|
| 256 |
+
# self.logger.error(f"Error in load_gene_viewer: {str(e)}")
|
| 257 |
+
# self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 258 |
|
| 259 |
def perform_off_target_analysis(self):
|
| 260 |
"""Launch off-target analysis for selected guides"""
|
|
|
|
| 299 |
def _handle_off_target_results(self, results):
|
| 300 |
"""Handle off-target analysis results"""
|
| 301 |
try:
|
| 302 |
+
scores, details = results
|
| 303 |
|
|
|
|
| 304 |
headers = self.view.get_table_headers()
|
| 305 |
|
| 306 |
# Find Score column index
|
|
|
|
| 367 |
"Please select guides to highlight in the gene viewer.")
|
| 368 |
return
|
| 369 |
|
| 370 |
+
# Get current gene/position
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
current_gene = self.view.combo_box_gene.currentText()
|
| 372 |
+
sequence = None
|
| 373 |
|
| 374 |
# Check if this is a position-based search
|
| 375 |
+
if "chromosome" in current_gene and "start:" in current_gene:
|
| 376 |
+
# Parse position from the text
|
| 377 |
try:
|
| 378 |
parts = current_gene.split(',')
|
| 379 |
+
chrom = parts[0].split('chromosome')[1].strip()
|
| 380 |
start = int(parts[1].split('start:')[1].strip())
|
| 381 |
end = int(parts[2].split('end:')[1].strip())
|
| 382 |
|
| 383 |
+
# Get sequence directly for position-based search
|
| 384 |
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 385 |
+
if sequence:
|
| 386 |
+
self.logger.debug(f"Got sequence of length {len(sequence)} for position-based search")
|
| 387 |
+
else:
|
| 388 |
+
raise ValueError(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
|
| 389 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
except Exception as e:
|
| 391 |
+
self.logger.error(f"Error parsing position or getting sequence: {str(e)}")
|
| 392 |
+
QMessageBox.warning(
|
| 393 |
+
self.view,
|
| 394 |
+
"Sequence Error",
|
| 395 |
+
f"Could not get sequence for the selected position: {str(e)}"
|
| 396 |
+
)
|
| 397 |
return
|
| 398 |
else:
|
| 399 |
# Regular gene-based search
|
|
|
|
| 407 |
QMessageBox.warning(self.view, "No Gene Data",
|
| 408 |
"Could not get gene sequence for highlighting.")
|
| 409 |
return
|
| 410 |
+
sequence = sequence_data['sequence']
|
| 411 |
|
| 412 |
+
self.logger.debug(f"Got sequence of length: {len(sequence)}")
|
| 413 |
|
| 414 |
+
# Convert table selections to the format expected by the model
|
| 415 |
+
guides_to_highlight = []
|
| 416 |
+
for guide in selected_rows:
|
| 417 |
+
guide_info = {
|
| 418 |
+
'location': guide['location'],
|
| 419 |
+
'sequence': guide['sequence'],
|
| 420 |
+
'strand': guide['strand']
|
| 421 |
+
}
|
| 422 |
+
guides_to_highlight.append(guide_info)
|
| 423 |
+
self.logger.debug(f"Guide to highlight: {guide_info}")
|
| 424 |
+
|
| 425 |
# Highlight the sequences
|
| 426 |
if guides_to_highlight:
|
| 427 |
self.logger.debug("Attempting to highlight sequences")
|
|
|
|
| 432 |
"Could not get sequence information from the selected rows.")
|
| 433 |
|
| 434 |
except Exception as e:
|
| 435 |
+
self.logger.error(f"Error in highlight_gene_viewer: {str(e)}")
|
| 436 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 437 |
show_error(self.settings, "Error highlighting gene viewer", str(e))
|
| 438 |
|
| 439 |
def export_targets(self):
|
|
|
|
| 524 |
return
|
| 525 |
|
| 526 |
# Get sequence for new range
|
| 527 |
+
if "chromosome" in current_gene and "start:" in current_gene:
|
| 528 |
# For position-based searches
|
| 529 |
try:
|
| 530 |
parts = current_gene.split(',')
|
| 531 |
+
# Get full chromosome identifier instead of just the number
|
| 532 |
+
chrom = parts[0].split('chromosome')[1].strip() # This will now keep the full identifier
|
| 533 |
+
|
| 534 |
# Get sequence for new range
|
| 535 |
sequence = self.model._get_sequence_for_position(chrom, new_start, new_end)
|
| 536 |
|
|
|
|
| 561 |
"Could not get gene data for the current selection."
|
| 562 |
)
|
| 563 |
return
|
| 564 |
+
|
| 565 |
+
print(f"locus_tag: {locus_tag}, new_start: {new_start}, new_end: {new_end}")
|
| 566 |
|
| 567 |
# Get new sequence for the range
|
| 568 |
sequence_data = self.model.get_gene_sequence_for_range(locus_tag, new_start, new_end)
|
|
|
|
| 591 |
current_gene = self.view.combo_box_gene.currentText()
|
| 592 |
|
| 593 |
# Check if this is a position-based search
|
| 594 |
+
if "chromosome" in current_gene and "start:" in current_gene:
|
| 595 |
try:
|
| 596 |
+
# Parse position from the text
|
| 597 |
parts = current_gene.split(',')
|
| 598 |
+
chrom = parts[0].split('chromosome')[1].strip() # Keep full chromosome ID
|
| 599 |
start = int(parts[1].split('start:')[1].strip())
|
| 600 |
end = int(parts[2].split('end:')[1].strip())
|
| 601 |
|
|
|
|
| 605 |
# Update gene viewer with sequence
|
| 606 |
self.view.set_text_edit_gene_viewer(sequence)
|
| 607 |
|
| 608 |
+
# Update location fields - subtract 1 from start to match 0-based indexing
|
| 609 |
+
self.view.line_edit_start_location.setText(str(start + 1))
|
| 610 |
self.view.line_edit_stop_location.setText(str(end))
|
| 611 |
else:
|
| 612 |
raise ValueError("Could not get sequence for position")
|
|
|
|
| 628 |
# Update gene viewer with sequence
|
| 629 |
self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
|
| 630 |
|
| 631 |
+
# Update location fields - subtract 1 from start to match 0-based indexing
|
| 632 |
+
self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
|
| 633 |
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 634 |
else:
|
| 635 |
self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
|
|
|
|
| 683 |
def on_gene_selected(self, selected_text):
|
| 684 |
"""Handle gene selection signal"""
|
| 685 |
try:
|
| 686 |
+
# Create loading dialog
|
| 687 |
+
loading_dialog = LoadingDialog(self.view, "Loading gene data...")
|
| 688 |
+
loading_dialog.show()
|
| 689 |
+
QApplication.processEvents()
|
| 690 |
|
| 691 |
+
try:
|
| 692 |
+
# Load data in chunks
|
| 693 |
+
loading_dialog.set_message("Loading sequence data...", 30)
|
| 694 |
+
QApplication.processEvents()
|
| 695 |
+
|
| 696 |
+
if "chromosome" in selected_text and "start:" in selected_text:
|
| 697 |
+
# Handle position-based search
|
| 698 |
parts = selected_text.split(',')
|
| 699 |
+
chrom = parts[0].split('chromosome')[1].strip()
|
| 700 |
start = int(parts[1].split('start:')[1].strip())
|
| 701 |
end = int(parts[2].split('end:')[1].strip())
|
| 702 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
# Update location fields
|
| 704 |
+
self.view.line_edit_start_location.setText(str(start))
|
| 705 |
+
self.view.line_edit_stop_location.setText(str(end))
|
| 706 |
|
| 707 |
+
# Filter guides efficiently
|
| 708 |
+
loading_dialog.set_message("Filtering guides...", 60)
|
| 709 |
+
QApplication.processEvents()
|
| 710 |
+
position_guides = [g for g in self.model.guides
|
| 711 |
+
if g.get('feature_id') == selected_text]
|
| 712 |
+
self.view.display_guides_in_table(position_guides)
|
| 713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
else:
|
| 715 |
+
# Regular gene-based search with optimized loading
|
| 716 |
+
locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 717 |
+
sequence_data = self.model.get_gene_sequence(locus_tag)
|
|
|
|
| 718 |
|
| 719 |
+
if sequence_data:
|
| 720 |
+
self.view.line_edit_start_location.setText(str(sequence_data['start']))
|
| 721 |
+
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 722 |
+
|
| 723 |
+
loading_dialog.set_message("Updating display...", 80)
|
| 724 |
+
QApplication.processEvents()
|
| 725 |
+
|
| 726 |
+
gene_guides = [g for g in self.model.guides
|
| 727 |
+
if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
|
| 728 |
+
self.view.display_guides_in_table(gene_guides)
|
| 729 |
+
|
| 730 |
+
# Refresh gene viewer
|
| 731 |
+
loading_dialog.set_message("Refreshing viewer...", 90)
|
| 732 |
+
QApplication.processEvents()
|
| 733 |
+
self.refresh_gene_viewer()
|
| 734 |
+
|
| 735 |
+
finally:
|
| 736 |
+
loading_dialog.close()
|
| 737 |
+
|
| 738 |
except Exception as e:
|
| 739 |
self.logger.error(f"Error handling gene selection: {str(e)}")
|
| 740 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 741 |
|
| 742 |
+
def highlight_guides_in_gene_viewer(self, guides_to_highlight):
|
| 743 |
"""Highlight selected guides in gene viewer"""
|
| 744 |
try:
|
| 745 |
+
# Get current sequence
|
| 746 |
+
current_gene = self.view.combo_box_gene.currentText()
|
| 747 |
+
sequence_data = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
+
# Get sequence based on view type
|
| 750 |
+
if "chromosome" in current_gene and "start:" in current_gene:
|
| 751 |
+
parts = current_gene.split(',')
|
| 752 |
+
chrom = parts[0].split('chromosome')[1].strip()
|
| 753 |
+
start = int(parts[1].split('start:')[1].strip())
|
| 754 |
+
end = int(parts[2].split('end:')[1].strip())
|
| 755 |
+
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 756 |
+
if sequence:
|
| 757 |
+
sequence_data = {'sequence': sequence}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
else:
|
| 759 |
+
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
|
|
|
| 760 |
sequence_data = self.model.get_gene_sequence(locus_tag)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
|
| 762 |
+
if not sequence_data or 'sequence' not in sequence_data:
|
| 763 |
+
self.logger.error("No sequence data available")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 765 |
|
| 766 |
+
sequence = sequence_data['sequence']
|
| 767 |
+
sequence_upper = sequence.upper()
|
| 768 |
|
| 769 |
+
# Clear existing highlights
|
| 770 |
+
self.view.dna_feature_viewer.sequence_viewer.clear_highlights()
|
|
|
|
| 771 |
|
| 772 |
+
for guide in guides_to_highlight:
|
| 773 |
+
try:
|
| 774 |
+
print(f"Guide: {guide}")
|
| 775 |
+
guide_sequence = guide['sequence']
|
| 776 |
+
strand = guide['strand']
|
| 777 |
+
print(f"Strand: {strand}")
|
| 778 |
+
|
| 779 |
+
# For negative strand guides
|
| 780 |
+
if strand == '-':
|
| 781 |
+
print("Negative strand")
|
| 782 |
+
# Convert sequence to complement for negative strand search
|
| 783 |
+
print(f"Sequence: {sequence_upper}")
|
| 784 |
+
complement_sequence = ''.join({'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G', 'K': 'M', 'Y': 'R', 'R': 'Y', 'M': 'K', 'S': 'S'}[base] for base in sequence_upper)
|
| 785 |
+
print(f"Complement sequence: {complement_sequence}")
|
| 786 |
+
target_sequence = guide_sequence.upper()
|
| 787 |
+
print(f"Target sequence: {target_sequence}")
|
| 788 |
+
target_sequence = target_sequence[::-1]
|
| 789 |
+
print(f"Reversed target sequence: {target_sequence}")
|
| 790 |
+
pos = complement_sequence.find(target_sequence)
|
| 791 |
+
print(f"Position: {pos}")
|
| 792 |
+
if pos != -1:
|
| 793 |
+
color = QColor(255, 0, 0, 100) # Red for negative strand
|
| 794 |
+
self.logger.debug(f"Found negative strand sequence at position {pos}")
|
| 795 |
+
|
| 796 |
+
# Pass the original position but indicate negative strand
|
| 797 |
+
self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
|
| 798 |
+
pos,
|
| 799 |
+
pos + len(guide_sequence) - 1,
|
| 800 |
+
color,
|
| 801 |
+
strand='-'
|
| 802 |
+
)
|
| 803 |
+
else:
|
| 804 |
+
self.logger.warning(f"Negative strand sequence {target_sequence} not found")
|
| 805 |
+
else:
|
| 806 |
+
# For positive strand guides
|
| 807 |
+
target_sequence = guide_sequence.upper()
|
| 808 |
+
pos = sequence_upper.find(target_sequence)
|
| 809 |
+
|
| 810 |
+
if pos != -1:
|
| 811 |
+
color = QColor(0, 255, 0, 100) # Green for positive strand
|
| 812 |
+
self.logger.debug(f"Found positive strand sequence at position {pos}")
|
| 813 |
+
|
| 814 |
+
self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
|
| 815 |
+
pos,
|
| 816 |
+
pos + len(guide_sequence) - 1,
|
| 817 |
+
color,
|
| 818 |
+
strand='+'
|
| 819 |
+
)
|
| 820 |
+
else:
|
| 821 |
+
self.logger.warning(f"Positive strand sequence {target_sequence} not found")
|
| 822 |
+
|
| 823 |
+
except Exception as e:
|
| 824 |
+
self.logger.error(f"Error highlighting guide: {str(e)}")
|
| 825 |
+
continue
|
| 826 |
+
|
| 827 |
except Exception as e:
|
| 828 |
+
self.logger.error(f"Error in highlight_guides_in_gene_viewer: {str(e)}")
|
| 829 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 830 |
|
| 831 |
def update_scores(self, scores, algorithm):
|
|
|
|
| 910 |
current_gene = self.view.combo_box_gene.currentText()
|
| 911 |
|
| 912 |
# Reset gene viewer to original sequence
|
| 913 |
+
if "chromosome" in current_gene and "start:" in current_gene:
|
| 914 |
# For position-based searches
|
| 915 |
try:
|
| 916 |
parts = current_gene.split(',')
|
| 917 |
+
chrom = parts[0].split('chromosome')[1].strip() # Keep full chromosome ID
|
| 918 |
start = int(parts[1].split('start:')[1].strip())
|
| 919 |
end = int(parts[2].split('end:')[1].strip())
|
| 920 |
|
|
|
|
| 1060 |
self.logger.error(f"Error handling co-targeting result: {str(e)}")
|
| 1061 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 1062 |
show_error(self.settings, "Co-targeting Error", str(e))
|
| 1063 |
+
|
| 1064 |
+
def refresh_gene_viewer(self):
|
| 1065 |
+
"""Refresh gene viewer with sequence and features"""
|
| 1066 |
+
try:
|
| 1067 |
+
current_gene = self.view.combo_box_gene.currentText()
|
| 1068 |
+
if not current_gene:
|
| 1069 |
+
return
|
| 1070 |
+
|
| 1071 |
+
self.logger.debug("Refreshing gene viewer")
|
| 1072 |
+
is_exons_only = self.view.check_box_view_exons_only.isChecked()
|
| 1073 |
+
self.logger.debug(f"View exons only is: {is_exons_only}")
|
| 1074 |
+
|
| 1075 |
+
# Get gene data
|
| 1076 |
+
if "chromosome" in current_gene and "start:" in current_gene:
|
| 1077 |
+
# Handle position-based search
|
| 1078 |
+
parts = current_gene.split(',')
|
| 1079 |
+
chrom = parts[0].split('chromosome')[1].strip()
|
| 1080 |
+
start = int(parts[1].split('start:')[1].strip())
|
| 1081 |
+
end = int(parts[2].split('end:')[1].strip())
|
| 1082 |
+
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 1083 |
+
|
| 1084 |
+
if sequence:
|
| 1085 |
+
# Get features for this region
|
| 1086 |
+
features = self.model.get_features_for_region(chrom, start, end)
|
| 1087 |
+
self.view.update_gene_viewer(sequence, features)
|
| 1088 |
+
else:
|
| 1089 |
+
# Regular gene-based search
|
| 1090 |
+
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
| 1091 |
+
self.logger.debug(f"Getting sequence for locus tag: {locus_tag}")
|
| 1092 |
+
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 1093 |
+
|
| 1094 |
+
if sequence_data and 'sequence' in sequence_data:
|
| 1095 |
+
# Get features for this gene
|
| 1096 |
+
features = self.model.get_features_for_gene(locus_tag)
|
| 1097 |
+
self.view.update_gene_viewer(sequence_data['sequence'], features)
|
| 1098 |
+
self.logger.debug(f"Updated gene viewer with sequence and {len(features)} features")
|
| 1099 |
+
else:
|
| 1100 |
+
self.logger.warning("No sequence data available")
|
| 1101 |
+
|
| 1102 |
+
except Exception as e:
|
| 1103 |
+
self.logger.error(f"Error refreshing gene viewer: {str(e)}")
|
| 1104 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
@@ -1,11 +1,9 @@
|
|
|
|
|
| 1 |
from PyQt6.QtWidgets import QMessageBox
|
| 2 |
from Bio import SeqIO
|
| 3 |
import os
|
| 4 |
import traceback
|
| 5 |
-
from functools import lru_cache
|
| 6 |
-
import json
|
| 7 |
import pickle
|
| 8 |
-
import time
|
| 9 |
|
| 10 |
class AnnotationParser:
|
| 11 |
def __init__(self, global_settings):
|
|
@@ -21,15 +19,22 @@ class AnnotationParser:
|
|
| 21 |
def set_annotation_file(self, file_path):
|
| 22 |
"""Set the annotation file and initialize/load index"""
|
| 23 |
try:
|
| 24 |
-
# Don't process if file_path is
|
| 25 |
-
if not file_path
|
| 26 |
-
self.logger.
|
| 27 |
self._index = {'locus_tags': {}} # Initialize empty index
|
| 28 |
return
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
if self.annotation_file_name != file_path:
|
| 31 |
-
total_start = time.time()
|
| 32 |
-
|
| 33 |
self.annotation_file_name = file_path
|
| 34 |
self.logger.debug(f"Set annotation file to: {file_path}")
|
| 35 |
|
|
@@ -37,15 +42,9 @@ class AnnotationParser:
|
|
| 37 |
self.index_file = f"{file_path}.index"
|
| 38 |
|
| 39 |
# Load or create index
|
| 40 |
-
index_start = time.time()
|
| 41 |
if not self._load_index():
|
| 42 |
self.logger.debug("Index not found or outdated, creating new index...")
|
| 43 |
-
create_start = time.time()
|
| 44 |
self._create_index()
|
| 45 |
-
create_time = time.time() - create_start
|
| 46 |
-
self.logger.debug(f"Index creation time: {create_time:.2f} seconds")
|
| 47 |
-
index_time = time.time() - index_start
|
| 48 |
-
self.logger.debug(f"Total index handling time: {index_time:.2f} seconds")
|
| 49 |
|
| 50 |
except Exception as e:
|
| 51 |
self.logger.error(f"Error in set_annotation_file: {str(e)}")
|
|
@@ -53,7 +52,6 @@ class AnnotationParser:
|
|
| 53 |
|
| 54 |
def _create_index(self):
|
| 55 |
try:
|
| 56 |
-
start_time = time.time()
|
| 57 |
self.logger.debug("Creating gene index file...")
|
| 58 |
|
| 59 |
# Initialize optimized index structure - no sequences stored
|
|
@@ -65,13 +63,22 @@ class AnnotationParser:
|
|
| 65 |
record_count = 0
|
| 66 |
feature_count = 0
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
for record in SeqIO.parse(self.annotation_file_name, "genbank"):
|
| 69 |
record_count += 1
|
| 70 |
-
record_start = time.time()
|
| 71 |
|
| 72 |
# Process features
|
| 73 |
for feature in record.features:
|
| 74 |
-
if feature.type in
|
| 75 |
feature_count += 1
|
| 76 |
|
| 77 |
# Get essential feature info
|
|
@@ -80,44 +87,104 @@ class AnnotationParser:
|
|
| 80 |
locus_tag = feature.qualifiers['locus_tag'][0]
|
| 81 |
elif 'gene' in feature.qualifiers:
|
| 82 |
locus_tag = feature.qualifiers['gene'][0]
|
| 83 |
-
|
| 84 |
# Only process features with valid locus tags
|
| 85 |
if locus_tag and locus_tag.lower() != "n/a":
|
| 86 |
-
#
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
#
|
| 92 |
feature_entry = {
|
| 93 |
'feature_type': feature.type,
|
| 94 |
'chromosome': record.id,
|
| 95 |
'location': f"{start}:{end}({strand})",
|
| 96 |
-
'
|
| 97 |
-
'
|
| 98 |
-
|
| 99 |
'start': start,
|
| 100 |
'end': end
|
| 101 |
}
|
| 102 |
-
|
| 103 |
-
#
|
| 104 |
-
index_data['locus_tags']
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
# Save compressed index to file
|
| 111 |
-
save_start = time.time()
|
| 112 |
with open(self.index_file, 'wb') as f:
|
| 113 |
pickle.dump(index_data, f, protocol=pickle.HIGHEST_PROTOCOL)
|
| 114 |
-
save_time = time.time() - save_start
|
| 115 |
-
total_time = time.time() - start_time
|
| 116 |
-
|
| 117 |
self._index = index_data
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
self.logger.debug(f"Index creation complete. Records: {record_count}, Features: {feature_count}")
|
| 120 |
-
self.logger.debug(f"Save time: {save_time:.2f}s, Total time: {total_time:.2f}s")
|
| 121 |
return True
|
| 122 |
|
| 123 |
except Exception as e:
|
|
@@ -125,21 +192,17 @@ class AnnotationParser:
|
|
| 125 |
return False
|
| 126 |
|
| 127 |
def _load_index(self):
|
| 128 |
-
"""Load the index file if it exists and is newer than the GenBank file"""
|
| 129 |
try:
|
|
|
|
| 130 |
if not os.path.exists(self.index_file):
|
| 131 |
return False
|
| 132 |
-
|
| 133 |
# Check if index is older than GenBank file
|
| 134 |
if os.path.getmtime(self.index_file) < os.path.getmtime(self.annotation_file_name):
|
| 135 |
return False
|
| 136 |
|
| 137 |
-
start_time = time.time()
|
| 138 |
with open(self.index_file, 'rb') as f:
|
| 139 |
self._index = pickle.load(f)
|
| 140 |
-
load_time = time.time() - start_time
|
| 141 |
-
print(f"Index file: {self._index}")
|
| 142 |
-
self.logger.debug(f"Index file loaded successfully in {load_time:.2f} seconds")
|
| 143 |
return True
|
| 144 |
|
| 145 |
except Exception as e:
|
|
@@ -162,16 +225,7 @@ class AnnotationParser:
|
|
| 162 |
|
| 163 |
# Search through index
|
| 164 |
if hasattr(self, '_index') and 'locus_tags' in self._index:
|
| 165 |
-
# Search through features, filtering for CDS and gene types only
|
| 166 |
for locus_tag, feature_entry in self._index['locus_tags'].items():
|
| 167 |
-
# Safely get feature type with default value
|
| 168 |
-
feature_type = feature_entry.get('feature_type', '')
|
| 169 |
-
|
| 170 |
-
# Only process CDS and gene features
|
| 171 |
-
if feature_type not in ['CDS', 'gene']:
|
| 172 |
-
continue
|
| 173 |
-
|
| 174 |
-
# Check gene name, locus tag, and description
|
| 175 |
searchable_text = ' '.join([
|
| 176 |
feature_entry.get('gene_name', '').lower(),
|
| 177 |
locus_tag.lower(),
|
|
@@ -183,8 +237,10 @@ class AnnotationParser:
|
|
| 183 |
info = {
|
| 184 |
'feature_id': locus_tag,
|
| 185 |
'feature_name': feature_entry.get('gene_name', 'N/A'),
|
|
|
|
| 186 |
'feature_location': feature_entry.get('location', 'N/A'),
|
| 187 |
-
'feature_description': feature_entry.get('description', 'N/A')
|
|
|
|
| 188 |
}
|
| 189 |
results_list.append((feature_entry.get('chromosome', ''), info))
|
| 190 |
|
|
@@ -192,7 +248,7 @@ class AnnotationParser:
|
|
| 192 |
|
| 193 |
except Exception as e:
|
| 194 |
self.logger.error(f"Error in genbank_search: {str(e)}")
|
| 195 |
-
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 196 |
raise
|
| 197 |
|
| 198 |
def get_gene_data(self, gene_identifier):
|
|
@@ -219,6 +275,7 @@ class AnnotationParser:
|
|
| 219 |
'feature_type': gene_info['feature_type'],
|
| 220 |
'chromosome': gene_info['chromosome'],
|
| 221 |
'location': gene_info['location'],
|
|
|
|
| 222 |
'gene_name': gene_info['gene_name'],
|
| 223 |
'description': gene_info['description'],
|
| 224 |
'start': gene_info['start'],
|
|
@@ -241,6 +298,7 @@ class AnnotationParser:
|
|
| 241 |
'feature_type': value['feature_type'],
|
| 242 |
'chromosome': value['chromosome'],
|
| 243 |
'location': value['location'],
|
|
|
|
| 244 |
'gene_name': value['gene_name'],
|
| 245 |
'description': value['description'],
|
| 246 |
'start': value['start'],
|
|
@@ -274,128 +332,9 @@ class AnnotationParser:
|
|
| 274 |
padded_sequence = sequence[start:end]
|
| 275 |
|
| 276 |
self.logger.debug(f"Padded sequence: {padded_sequence}")
|
| 277 |
-
|
| 278 |
return padded_sequence
|
| 279 |
-
|
| 280 |
return None
|
| 281 |
|
| 282 |
except Exception as e:
|
| 283 |
self.logger.error(f"Error getting sequence for gene: {str(e)}")
|
| 284 |
-
return None
|
| 285 |
-
|
| 286 |
-
@lru_cache(maxsize=1)
|
| 287 |
-
def _get_records(self):
|
| 288 |
-
"""Cache and return all records from the annotation file"""
|
| 289 |
-
start_time = time.time()
|
| 290 |
-
if not self._record_cache:
|
| 291 |
-
try:
|
| 292 |
-
self.logger.debug("Loading records from file...")
|
| 293 |
-
self._record_cache = list(SeqIO.parse(self.annotation_file_name, "genbank"))
|
| 294 |
-
load_time = time.time() - start_time
|
| 295 |
-
self.logger.debug(f"Time to load records: {load_time:.2f} seconds")
|
| 296 |
-
except Exception as e:
|
| 297 |
-
self.logger.error(f"Error reading annotation file: {str(e)}")
|
| 298 |
-
return []
|
| 299 |
-
return self._record_cache
|
| 300 |
-
|
| 301 |
-
def get_max_chrom(self):
|
| 302 |
-
try:
|
| 303 |
-
parser = SeqIO.parse(self.annotation_file_name, 'genbank')
|
| 304 |
-
max_chrom = sum(1 for _ in parser)
|
| 305 |
-
return max_chrom
|
| 306 |
-
except Exception as e:
|
| 307 |
-
self.logger.error(f"Error in get_max_chrom: {str(e)}")
|
| 308 |
-
self._show_error("Error in get_max_chrom", str(e))
|
| 309 |
-
return 0
|
| 310 |
-
|
| 311 |
-
def get_sequence_info(self, query):
|
| 312 |
-
# Implement this method if needed
|
| 313 |
-
pass
|
| 314 |
-
|
| 315 |
-
def find_which_file_version(self):
|
| 316 |
-
try:
|
| 317 |
-
if not self.annotation_file_name or os.path.basename(self.annotation_file_name) == "None":
|
| 318 |
-
return -1
|
| 319 |
-
if self.annotation_file_name.endswith(('.gbff', '.gbk')):
|
| 320 |
-
return "gbff"
|
| 321 |
-
else:
|
| 322 |
-
return -1
|
| 323 |
-
except Exception as e:
|
| 324 |
-
self.logger.error(f"Error in find_which_file_version: {str(e)}")
|
| 325 |
-
self._show_error("Error in find_which_file_version", str(e))
|
| 326 |
-
return -1
|
| 327 |
-
|
| 328 |
-
def _show_error(self, title, message):
|
| 329 |
-
QMessageBox.critical(None, title, f"{message}\n\nFor more information, check the log file.")
|
| 330 |
-
|
| 331 |
-
@staticmethod
|
| 332 |
-
def flatten_list(t):
|
| 333 |
-
return [item.lower() for sublist in t for item in sublist]
|
| 334 |
-
|
| 335 |
-
def _get_feature_info(self, feature):
|
| 336 |
-
return {
|
| 337 |
-
'feature_id': self._get_feature_id(feature),
|
| 338 |
-
'feature_name': self._get_feature_name(feature),
|
| 339 |
-
'feature_location': self._get_feature_location(feature),
|
| 340 |
-
'feature_description': self._get_feature_description(feature)
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
def _get_feature_id(self, feature):
|
| 344 |
-
for key in ['locus_tag']:
|
| 345 |
-
if key in feature.qualifiers:
|
| 346 |
-
return feature.qualifiers[key][0]
|
| 347 |
-
return "N/A"
|
| 348 |
-
|
| 349 |
-
def _get_feature_name(self, feature):
|
| 350 |
-
for key in ['gene']:
|
| 351 |
-
if key in feature.qualifiers:
|
| 352 |
-
return feature.qualifiers[key][0]
|
| 353 |
-
return "N/A"
|
| 354 |
-
|
| 355 |
-
def _get_feature_location(self, feature):
|
| 356 |
-
if feature.location:
|
| 357 |
-
start = feature.location.start
|
| 358 |
-
end = feature.location.end
|
| 359 |
-
strand = '+' if feature.location.strand == 1 else '-'
|
| 360 |
-
return f"{start}:{end}({strand})"
|
| 361 |
-
return "N/A"
|
| 362 |
-
|
| 363 |
-
def _get_feature_description(self, feature):
|
| 364 |
-
for key in ['product', 'note']:
|
| 365 |
-
if key in feature.qualifiers:
|
| 366 |
-
return feature.qualifiers[key][0]
|
| 367 |
-
return "N/A"
|
| 368 |
-
|
| 369 |
-
def get_available_genes(self):
|
| 370 |
-
return self.available_genes
|
| 371 |
-
|
| 372 |
-
def get_full_gene_sequence(self):
|
| 373 |
-
# Implement this method if needed
|
| 374 |
-
pass
|
| 375 |
-
|
| 376 |
-
def _build_gene_index(self, records):
|
| 377 |
-
"""Build an index of genes for faster lookup"""
|
| 378 |
-
self._gene_index = {}
|
| 379 |
-
try:
|
| 380 |
-
for record in records:
|
| 381 |
-
for feature in record.features:
|
| 382 |
-
if feature.type == 'gene':
|
| 383 |
-
gene_name = self._get_feature_name(feature)
|
| 384 |
-
gene_id = self._get_feature_id(feature)
|
| 385 |
-
if gene_name != "N/A":
|
| 386 |
-
self._gene_index[gene_name] = (record.id, feature)
|
| 387 |
-
if gene_id != "N/A":
|
| 388 |
-
self._gene_index[gene_id] = (record.id, feature)
|
| 389 |
-
except Exception as e:
|
| 390 |
-
self.logger.error(f"Error building gene index: {str(e)}")
|
| 391 |
-
|
| 392 |
-
def _parse_available_genes(self):
|
| 393 |
-
self.available_genes = []
|
| 394 |
-
try:
|
| 395 |
-
for record in SeqIO.parse(self.annotation_file_name, "genbank"):
|
| 396 |
-
for feature in record.features:
|
| 397 |
-
if feature.type == 'gene':
|
| 398 |
-
self.available_genes.append(self._get_feature_name(feature))
|
| 399 |
-
except Exception as e:
|
| 400 |
-
self.logger.error(f"Error parsing available genes: {str(e)}")
|
| 401 |
-
|
|
|
|
| 1 |
+
import Bio
|
| 2 |
from PyQt6.QtWidgets import QMessageBox
|
| 3 |
from Bio import SeqIO
|
| 4 |
import os
|
| 5 |
import traceback
|
|
|
|
|
|
|
| 6 |
import pickle
|
|
|
|
| 7 |
|
| 8 |
class AnnotationParser:
|
| 9 |
def __init__(self, global_settings):
|
|
|
|
| 19 |
def set_annotation_file(self, file_path):
|
| 20 |
"""Set the annotation file and initialize/load index"""
|
| 21 |
try:
|
| 22 |
+
# Don't process if file_path is empty
|
| 23 |
+
if not file_path:
|
| 24 |
+
self.logger.warning("Empty annotation file path provided")
|
| 25 |
self._index = {'locus_tags': {}} # Initialize empty index
|
| 26 |
return
|
| 27 |
|
| 28 |
+
# Normalize path and remove any trailing slashes
|
| 29 |
+
file_path = os.path.normpath(file_path)
|
| 30 |
+
|
| 31 |
+
# Verify file exists and is a file (not a directory)
|
| 32 |
+
if not os.path.isfile(file_path):
|
| 33 |
+
self.logger.error(f"Invalid annotation file path: {file_path}")
|
| 34 |
+
self._index = {'locus_tags': {}}
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
if self.annotation_file_name != file_path:
|
|
|
|
|
|
|
| 38 |
self.annotation_file_name = file_path
|
| 39 |
self.logger.debug(f"Set annotation file to: {file_path}")
|
| 40 |
|
|
|
|
| 42 |
self.index_file = f"{file_path}.index"
|
| 43 |
|
| 44 |
# Load or create index
|
|
|
|
| 45 |
if not self._load_index():
|
| 46 |
self.logger.debug("Index not found or outdated, creating new index...")
|
|
|
|
| 47 |
self._create_index()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
except Exception as e:
|
| 50 |
self.logger.error(f"Error in set_annotation_file: {str(e)}")
|
|
|
|
| 52 |
|
| 53 |
def _create_index(self):
|
| 54 |
try:
|
|
|
|
| 55 |
self.logger.debug("Creating gene index file...")
|
| 56 |
|
| 57 |
# Initialize optimized index structure - no sequences stored
|
|
|
|
| 63 |
record_count = 0
|
| 64 |
feature_count = 0
|
| 65 |
|
| 66 |
+
# Priority order for feature types (higher index = higher priority)
|
| 67 |
+
feature_priority = {
|
| 68 |
+
'CDS': 0,
|
| 69 |
+
'gene': 1,
|
| 70 |
+
'mRNA': 2,
|
| 71 |
+
'tRNA': 2,
|
| 72 |
+
'rRNA': 2,
|
| 73 |
+
'ncRNA': 2
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
for record in SeqIO.parse(self.annotation_file_name, "genbank"):
|
| 77 |
record_count += 1
|
|
|
|
| 78 |
|
| 79 |
# Process features
|
| 80 |
for feature in record.features:
|
| 81 |
+
if feature.type in feature_priority:
|
| 82 |
feature_count += 1
|
| 83 |
|
| 84 |
# Get essential feature info
|
|
|
|
| 87 |
locus_tag = feature.qualifiers['locus_tag'][0]
|
| 88 |
elif 'gene' in feature.qualifiers:
|
| 89 |
locus_tag = feature.qualifiers['gene'][0]
|
| 90 |
+
|
| 91 |
# Only process features with valid locus tags
|
| 92 |
if locus_tag and locus_tag.lower() != "n/a":
|
| 93 |
+
# Handle joined locations
|
| 94 |
+
if isinstance(feature.location, Bio.SeqFeature.CompoundLocation):
|
| 95 |
+
if locus_tag == "CAALFM_C304810CA":
|
| 96 |
+
print(f"Feature location: {feature.location}")
|
| 97 |
+
# Get all parts of the joined location
|
| 98 |
+
parts = feature.location.parts
|
| 99 |
+
# Find min start and max end across all parts
|
| 100 |
+
start = min(int(part.start) for part in parts)
|
| 101 |
+
end = max(int(part.end) for part in parts)
|
| 102 |
+
|
| 103 |
+
# Format parts with strand info
|
| 104 |
+
formatted_parts = [
|
| 105 |
+
f"{int(part.start)}..{int(part.end)}({'+' if part.strand == 1 else '-'})"
|
| 106 |
+
for part in parts
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
# If on minus strand, reverse the order of parts
|
| 110 |
+
if feature.location.strand == -1:
|
| 111 |
+
formatted_parts.reverse()
|
| 112 |
+
|
| 113 |
+
# Join parts into full location string
|
| 114 |
+
full_location = ','.join(formatted_parts)
|
| 115 |
+
|
| 116 |
+
# Get overall strand for location field
|
| 117 |
+
strand = '+' if feature.location.strand == 1 else '-'
|
| 118 |
+
else:
|
| 119 |
+
start = int(feature.location.start)
|
| 120 |
+
end = int(feature.location.end)
|
| 121 |
+
strand = '+' if feature.location.strand == 1 else '-'
|
| 122 |
+
full_location = f"{start}..{end}({strand})"
|
| 123 |
+
|
| 124 |
+
# Get description first since we might need it for the name
|
| 125 |
+
description = feature.qualifiers.get('product',
|
| 126 |
+
feature.qualifiers.get('note', ['N/A']))[0]
|
| 127 |
+
|
| 128 |
+
# Get gene name, use description if gene name is N/A
|
| 129 |
+
gene_name = feature.qualifiers.get('gene', ['N/A'])[0]
|
| 130 |
+
if gene_name == 'N/A':
|
| 131 |
+
gene_name = description # Use description as name if no gene name
|
| 132 |
|
| 133 |
+
# Create new feature entry
|
| 134 |
feature_entry = {
|
| 135 |
'feature_type': feature.type,
|
| 136 |
'chromosome': record.id,
|
| 137 |
'location': f"{start}:{end}({strand})",
|
| 138 |
+
'full_location': full_location,
|
| 139 |
+
'gene_name': gene_name,
|
| 140 |
+
'description': description,
|
| 141 |
'start': start,
|
| 142 |
'end': end
|
| 143 |
}
|
| 144 |
+
|
| 145 |
+
# Update index based on priority
|
| 146 |
+
if locus_tag in index_data['locus_tags']:
|
| 147 |
+
existing_entry = index_data['locus_tags'][locus_tag]
|
| 148 |
+
existing_priority = feature_priority[existing_entry['feature_type']]
|
| 149 |
+
current_priority = feature_priority[feature.type]
|
| 150 |
+
|
| 151 |
+
if current_priority >= existing_priority:
|
| 152 |
+
# Keep the RNA/higher priority feature type
|
| 153 |
+
merged_entry = existing_entry.copy()
|
| 154 |
+
merged_entry['feature_type'] = feature.type
|
| 155 |
+
|
| 156 |
+
# Update other fields only if they're not 'N/A'
|
| 157 |
+
if feature_entry['gene_name'] != 'N/A':
|
| 158 |
+
merged_entry['gene_name'] = feature_entry['gene_name']
|
| 159 |
+
if feature_entry['description'] != 'N/A':
|
| 160 |
+
merged_entry['description'] = feature_entry['description']
|
| 161 |
+
# If gene name is N/A, use the new description
|
| 162 |
+
if merged_entry['gene_name'] == 'N/A':
|
| 163 |
+
merged_entry['gene_name'] = feature_entry['description']
|
| 164 |
+
|
| 165 |
+
# Always update location information
|
| 166 |
+
merged_entry.update({
|
| 167 |
+
'location': feature_entry['location'],
|
| 168 |
+
'full_location': feature_entry['full_location'],
|
| 169 |
+
'start': feature_entry['start'],
|
| 170 |
+
'end': feature_entry['end']
|
| 171 |
+
})
|
| 172 |
+
|
| 173 |
+
index_data['locus_tags'][locus_tag] = merged_entry
|
| 174 |
+
else:
|
| 175 |
+
# New entry
|
| 176 |
+
index_data['locus_tags'][locus_tag] = feature_entry
|
| 177 |
|
| 178 |
# Save compressed index to file
|
|
|
|
| 179 |
with open(self.index_file, 'wb') as f:
|
| 180 |
pickle.dump(index_data, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
|
|
|
|
|
|
|
|
| 181 |
self._index = index_data
|
| 182 |
+
|
| 183 |
+
# if locus tag is CAALFM_C304810CA
|
| 184 |
+
if 'CAALFM_C304810CA' in index_data['locus_tags']:
|
| 185 |
+
print(f"Locus tag CAALFM_C304810CA found: {index_data['locus_tags']['CAALFM_C304810CA']}")
|
| 186 |
+
|
| 187 |
self.logger.debug(f"Index creation complete. Records: {record_count}, Features: {feature_count}")
|
|
|
|
| 188 |
return True
|
| 189 |
|
| 190 |
except Exception as e:
|
|
|
|
| 192 |
return False
|
| 193 |
|
| 194 |
def _load_index(self):
|
|
|
|
| 195 |
try:
|
| 196 |
+
self.logger.debug(f"Loading index from: {self.index_file}")
|
| 197 |
if not os.path.exists(self.index_file):
|
| 198 |
return False
|
| 199 |
+
|
| 200 |
# Check if index is older than GenBank file
|
| 201 |
if os.path.getmtime(self.index_file) < os.path.getmtime(self.annotation_file_name):
|
| 202 |
return False
|
| 203 |
|
|
|
|
| 204 |
with open(self.index_file, 'rb') as f:
|
| 205 |
self._index = pickle.load(f)
|
|
|
|
|
|
|
|
|
|
| 206 |
return True
|
| 207 |
|
| 208 |
except Exception as e:
|
|
|
|
| 225 |
|
| 226 |
# Search through index
|
| 227 |
if hasattr(self, '_index') and 'locus_tags' in self._index:
|
|
|
|
| 228 |
for locus_tag, feature_entry in self._index['locus_tags'].items():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
searchable_text = ' '.join([
|
| 230 |
feature_entry.get('gene_name', '').lower(),
|
| 231 |
locus_tag.lower(),
|
|
|
|
| 237 |
info = {
|
| 238 |
'feature_id': locus_tag,
|
| 239 |
'feature_name': feature_entry.get('gene_name', 'N/A'),
|
| 240 |
+
'feature_full_location': feature_entry.get('full_location', 'N/A'),
|
| 241 |
'feature_location': feature_entry.get('location', 'N/A'),
|
| 242 |
+
'feature_description': feature_entry.get('description', 'N/A'),
|
| 243 |
+
'feature_type': feature_entry.get('feature_type', 'CDS')
|
| 244 |
}
|
| 245 |
results_list.append((feature_entry.get('chromosome', ''), info))
|
| 246 |
|
|
|
|
| 248 |
|
| 249 |
except Exception as e:
|
| 250 |
self.logger.error(f"Error in genbank_search: {str(e)}")
|
| 251 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 252 |
raise
|
| 253 |
|
| 254 |
def get_gene_data(self, gene_identifier):
|
|
|
|
| 275 |
'feature_type': gene_info['feature_type'],
|
| 276 |
'chromosome': gene_info['chromosome'],
|
| 277 |
'location': gene_info['location'],
|
| 278 |
+
'full_location': gene_info.get('full_location', ''), # Add full location
|
| 279 |
'gene_name': gene_info['gene_name'],
|
| 280 |
'description': gene_info['description'],
|
| 281 |
'start': gene_info['start'],
|
|
|
|
| 298 |
'feature_type': value['feature_type'],
|
| 299 |
'chromosome': value['chromosome'],
|
| 300 |
'location': value['location'],
|
| 301 |
+
'full_location': value.get('full_location', ''), # Add full location
|
| 302 |
'gene_name': value['gene_name'],
|
| 303 |
'description': value['description'],
|
| 304 |
'start': value['start'],
|
|
|
|
| 332 |
padded_sequence = sequence[start:end]
|
| 333 |
|
| 334 |
self.logger.debug(f"Padded sequence: {padded_sequence}")
|
|
|
|
| 335 |
return padded_sequence
|
|
|
|
| 336 |
return None
|
| 337 |
|
| 338 |
except Exception as e:
|
| 339 |
self.logger.error(f"Error getting sequence for gene: {str(e)}")
|
| 340 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2,7 +2,6 @@ from utils.sequence_utils import SeqTranslate
|
|
| 2 |
import logging
|
| 3 |
from multiprocessing import Pool, cpu_count
|
| 4 |
from functools import partial
|
| 5 |
-
import time
|
| 6 |
import pickle
|
| 7 |
import os
|
| 8 |
import traceback
|
|
@@ -16,9 +15,7 @@ class CSPRparser:
|
|
| 16 |
self.index_file = f"{inputFileName}.index"
|
| 17 |
|
| 18 |
def _create_index(self):
|
| 19 |
-
"""Create an index file for faster searching"""
|
| 20 |
try:
|
| 21 |
-
start_time = time.time()
|
| 22 |
self.logger.debug("Creating CSPR index file...")
|
| 23 |
|
| 24 |
# Initialize index structure
|
|
@@ -67,8 +64,6 @@ class CSPRparser:
|
|
| 67 |
|
| 68 |
self._index = index_data
|
| 69 |
|
| 70 |
-
create_time = time.time() - start_time
|
| 71 |
-
self.logger.debug(f"Index creation time: {create_time:.2f} seconds")
|
| 72 |
return True
|
| 73 |
|
| 74 |
except Exception as e:
|
|
@@ -93,8 +88,9 @@ class CSPRparser:
|
|
| 93 |
|
| 94 |
def read_targets_batch(self, chromosome, targets, endonuclease):
|
| 95 |
try:
|
| 96 |
-
|
| 97 |
-
|
|
|
|
| 98 |
# Load or create index
|
| 99 |
if not hasattr(self, '_index'):
|
| 100 |
if not self._load_index():
|
|
@@ -106,27 +102,20 @@ class CSPRparser:
|
|
| 106 |
max_end = max(t['end'] for t in sorted_targets)
|
| 107 |
|
| 108 |
self.logger.debug(f"Processing targets from {min_start} to {max_end}")
|
| 109 |
-
self.logger.debug(f"Looking for chromosome
|
| 110 |
|
| 111 |
results = []
|
| 112 |
lines_processed = 0
|
| 113 |
lines_skipped = 0
|
| 114 |
|
| 115 |
-
# Find chromosome
|
| 116 |
found_chrom = None
|
| 117 |
-
chrom_count = 0
|
| 118 |
-
target_chrom_num = int(chromosome) # Convert chromosome to integer
|
| 119 |
-
|
| 120 |
-
# Debug available chromosomes
|
| 121 |
-
self.logger.debug(f"Available chromosomes: {list(self._index.keys())}")
|
| 122 |
-
|
| 123 |
for chrom_id in self._index:
|
| 124 |
# Decode bytes to string if necessary
|
| 125 |
chrom_str = chrom_id.decode() if isinstance(chrom_id, bytes) else chrom_id
|
| 126 |
|
| 127 |
-
#
|
| 128 |
-
|
| 129 |
-
if chrom_count == target_chrom_num:
|
| 130 |
found_chrom = chrom_id
|
| 131 |
self.logger.debug(f"Found matching chromosome: {chrom_str}")
|
| 132 |
break
|
|
@@ -179,16 +168,13 @@ class CSPRparser:
|
|
| 179 |
self.logger.error(f"Chromosome {chromosome} not found in index")
|
| 180 |
self.logger.debug(f"Available chromosomes: {list(self._index.keys())}")
|
| 181 |
|
| 182 |
-
total_time = time.time() - start_time
|
| 183 |
-
self.logger.debug(f"Processed {lines_processed} lines, skipped {lines_skipped}")
|
| 184 |
-
self.logger.debug(f"Found {len(results)} targets in {total_time:.2f} seconds")
|
| 185 |
-
|
| 186 |
return results
|
| 187 |
|
| 188 |
except Exception as e:
|
| 189 |
self.logger.error(f"Error in read_targets_batch: {str(e)}")
|
| 190 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 191 |
return []
|
|
|
|
| 192 |
def parse_targets(self, file_path, region):
|
| 193 |
"""Parse targets with parallel processing and caching"""
|
| 194 |
cache_key = f"{file_path}:{region}"
|
|
|
|
| 2 |
import logging
|
| 3 |
from multiprocessing import Pool, cpu_count
|
| 4 |
from functools import partial
|
|
|
|
| 5 |
import pickle
|
| 6 |
import os
|
| 7 |
import traceback
|
|
|
|
| 15 |
self.index_file = f"{inputFileName}.index"
|
| 16 |
|
| 17 |
def _create_index(self):
|
|
|
|
| 18 |
try:
|
|
|
|
| 19 |
self.logger.debug("Creating CSPR index file...")
|
| 20 |
|
| 21 |
# Initialize index structure
|
|
|
|
| 64 |
|
| 65 |
self._index = index_data
|
| 66 |
|
|
|
|
|
|
|
| 67 |
return True
|
| 68 |
|
| 69 |
except Exception as e:
|
|
|
|
| 88 |
|
| 89 |
def read_targets_batch(self, chromosome, targets, endonuclease):
|
| 90 |
try:
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
print(f"Reading targets for chromosome: {chromosome}")
|
| 94 |
# Load or create index
|
| 95 |
if not hasattr(self, '_index'):
|
| 96 |
if not self._load_index():
|
|
|
|
| 102 |
max_end = max(t['end'] for t in sorted_targets)
|
| 103 |
|
| 104 |
self.logger.debug(f"Processing targets from {min_start} to {max_end}")
|
| 105 |
+
self.logger.debug(f"Looking for chromosome: {chromosome}")
|
| 106 |
|
| 107 |
results = []
|
| 108 |
lines_processed = 0
|
| 109 |
lines_skipped = 0
|
| 110 |
|
| 111 |
+
# Find chromosome by full ID
|
| 112 |
found_chrom = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
for chrom_id in self._index:
|
| 114 |
# Decode bytes to string if necessary
|
| 115 |
chrom_str = chrom_id.decode() if isinstance(chrom_id, bytes) else chrom_id
|
| 116 |
|
| 117 |
+
# Match the full chromosome ID
|
| 118 |
+
if chrom_str == chromosome:
|
|
|
|
| 119 |
found_chrom = chrom_id
|
| 120 |
self.logger.debug(f"Found matching chromosome: {chrom_str}")
|
| 121 |
break
|
|
|
|
| 168 |
self.logger.error(f"Chromosome {chromosome} not found in index")
|
| 169 |
self.logger.debug(f"Available chromosomes: {list(self._index.keys())}")
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
return results
|
| 172 |
|
| 173 |
except Exception as e:
|
| 174 |
self.logger.error(f"Error in read_targets_batch: {str(e)}")
|
| 175 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 176 |
return []
|
| 177 |
+
|
| 178 |
def parse_targets(self, file_path, region):
|
| 179 |
"""Parse targets with parallel processing and caching"""
|
| 180 |
cache_key = f"{file_path}:{region}"
|
|
@@ -125,17 +125,34 @@ class DatabaseManager(QObject):
|
|
| 125 |
return adjusted_path
|
| 126 |
|
| 127 |
def _update_watched_directory(self):
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
self.file_watcher.
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
def _detect_file_changes(self) -> Dict[FileChangeType, List[str]]:
|
| 141 |
"""Detect what files have changed and categorize the changes"""
|
|
@@ -173,25 +190,23 @@ class DatabaseManager(QObject):
|
|
| 173 |
try:
|
| 174 |
self.logger.debug(f"Detected change in directory: {path}")
|
| 175 |
|
|
|
|
|
|
|
|
|
|
| 176 |
# Detect specific changes
|
| 177 |
changes = self._detect_file_changes()
|
| 178 |
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
| 180 |
self.logger.debug(f"Detected file changes: {changes}")
|
| 181 |
-
|
| 182 |
-
# Get validation state
|
| 183 |
-
is_valid, message = self.validate_db_path(path)
|
| 184 |
-
|
| 185 |
-
# Emit separate signals
|
| 186 |
-
self.db_validation_changed.emit(is_valid, message)
|
| 187 |
self.db_files_changed.emit(changes)
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
else:
|
| 194 |
-
self.logger.debug("No relevant file changes detected")
|
| 195 |
|
| 196 |
except Exception as e:
|
| 197 |
self.logger.error(f"Error handling directory change: {str(e)}")
|
|
|
|
| 125 |
return adjusted_path
|
| 126 |
|
| 127 |
def _update_watched_directory(self):
|
| 128 |
+
"""Update the watched directory and validate the new path"""
|
| 129 |
+
try:
|
| 130 |
+
# Remove old watched directories
|
| 131 |
+
self.file_watcher.removePaths(self.file_watcher.directories())
|
| 132 |
|
| 133 |
+
if self.db_path and os.path.isdir(self.db_path):
|
| 134 |
+
# Add new directory to watch
|
| 135 |
+
self.file_watcher.addPath(self.db_path)
|
| 136 |
+
|
| 137 |
+
# Add GBFF subdirectory if it exists
|
| 138 |
+
gbff_path = os.path.join(self.db_path, 'GBFF')
|
| 139 |
+
if os.path.isdir(gbff_path):
|
| 140 |
+
self.file_watcher.addPath(gbff_path)
|
| 141 |
+
|
| 142 |
+
self.logger.debug(f"Now watching directories: {self.file_watcher.directories()}")
|
| 143 |
|
| 144 |
+
# Validate the new path and emit signals
|
| 145 |
+
is_valid, message = self.validate_db_path(self.db_path)
|
| 146 |
+
self.db_validation_changed.emit(is_valid, message)
|
| 147 |
+
|
| 148 |
+
# Also check for any file changes
|
| 149 |
+
changes = self._detect_file_changes()
|
| 150 |
+
if changes:
|
| 151 |
+
self.db_files_changed.emit(changes)
|
| 152 |
+
self.db_state_changed.emit(is_valid, message, changes)
|
| 153 |
+
|
| 154 |
+
except Exception as e:
|
| 155 |
+
self.logger.error(f"Error updating watched directory: {str(e)}")
|
| 156 |
|
| 157 |
def _detect_file_changes(self) -> Dict[FileChangeType, List[str]]:
|
| 158 |
"""Detect what files have changed and categorize the changes"""
|
|
|
|
| 190 |
try:
|
| 191 |
self.logger.debug(f"Detected change in directory: {path}")
|
| 192 |
|
| 193 |
+
# Re-validate the path
|
| 194 |
+
is_valid, message = self.validate_db_path(self.db_path)
|
| 195 |
+
|
| 196 |
# Detect specific changes
|
| 197 |
changes = self._detect_file_changes()
|
| 198 |
|
| 199 |
+
# Always emit validation signal on directory change
|
| 200 |
+
self.db_validation_changed.emit(is_valid, message)
|
| 201 |
+
|
| 202 |
+
if changes: # Only emit change signals if there are actual changes
|
| 203 |
self.logger.debug(f"Detected file changes: {changes}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
self.db_files_changed.emit(changes)
|
| 205 |
|
| 206 |
+
# Always emit combined state signal
|
| 207 |
+
self.db_state_changed.emit(is_valid, message, changes or {})
|
| 208 |
+
|
| 209 |
+
self.logger.info(f"Database state updated - Valid: {is_valid}, Changes: {changes}")
|
|
|
|
|
|
|
| 210 |
|
| 211 |
except Exception as e:
|
| 212 |
self.logger.error(f"Error handling directory change: {str(e)}")
|
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import time
|
| 2 |
from models.HomeWindowModel import HomeWindowModel
|
| 3 |
from models.CSPRparser import CSPRparser
|
| 4 |
from models.AnnotationParser import AnnotationParser
|
|
@@ -67,51 +66,60 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 67 |
|
| 68 |
def find_targets_by_feature(self, parser, input_data):
|
| 69 |
try:
|
| 70 |
-
|
| 71 |
-
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
annotation_file_path = os.path.
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
for record in SeqIO.parse(annotation_file_path, "genbank"):
|
| 86 |
-
chrom_count += 1
|
| 87 |
-
chrom_mapping[record.id] = str(chrom_count)
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
target_info = {
|
| 97 |
-
'feature_type': 'CDS',
|
| 98 |
-
'chromosome': chrom_num,
|
| 99 |
-
'full_chromosome': record_id,
|
| 100 |
-
'feature_id': feature_info['feature_id'],
|
| 101 |
-
'feature_name': feature_info['feature_name'],
|
| 102 |
-
'feature_description': feature_info['feature_description'],
|
| 103 |
-
'location': f"{start}-{end}",
|
| 104 |
-
'start': start,
|
| 105 |
-
'end': end,
|
| 106 |
-
'strand': '+' if '(+)' in location else '-',
|
| 107 |
-
'endonuclease': input_data['endonuclease']
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
self.global_settings.logger.debug(f"Created target info: {target_info}")
|
| 111 |
-
|
| 112 |
-
formatted_results.append(target_info)
|
| 113 |
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
except Exception as e:
|
| 117 |
self.global_settings.logger.error(f"Error in find_targets_by_feature: {str(e)}")
|
|
@@ -124,71 +132,66 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 124 |
|
| 125 |
for query in queries:
|
| 126 |
try:
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
# Get full chromosome ID by counting carets
|
| 130 |
-
full_chrom = None
|
| 131 |
-
chrom_count = 0
|
| 132 |
|
| 133 |
# Get annotation file path
|
| 134 |
annotation_file = self.global_settings.get_current_annotation_file()
|
| 135 |
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 136 |
|
| 137 |
-
# Find the
|
|
|
|
|
|
|
|
|
|
| 138 |
for record in SeqIO.parse(annotation_path, "genbank"):
|
| 139 |
-
|
| 140 |
-
if
|
| 141 |
full_chrom = record.id
|
| 142 |
-
self.logger.debug(f"Found chromosome {
|
| 143 |
break
|
| 144 |
|
| 145 |
if not full_chrom:
|
| 146 |
-
self.logger.warning(f"Could not find chromosome at position {
|
| 147 |
continue
|
| 148 |
|
| 149 |
-
# Create
|
| 150 |
-
position_name = f"
|
| 151 |
target_info = [{
|
| 152 |
'start': start,
|
| 153 |
'end': end,
|
| 154 |
-
'feature_id': position_name,
|
| 155 |
'feature_name': position_name,
|
| 156 |
-
'chromosome':
|
| 157 |
-
'
|
| 158 |
}]
|
| 159 |
|
| 160 |
# Get targets using batch processing
|
| 161 |
-
self.logger.debug(f"Searching for targets in chromosome {
|
| 162 |
-
targets = parser.read_targets_batch(
|
| 163 |
|
| 164 |
if targets:
|
| 165 |
self.logger.debug(f"Found {len(targets)} raw targets")
|
| 166 |
filtered_targets = []
|
| 167 |
-
guide_length = 23
|
| 168 |
|
| 169 |
for target in targets:
|
| 170 |
target_pos = int(target['position'])
|
| 171 |
-
target_end = target_pos
|
| 172 |
|
| 173 |
-
# Include target if
|
| 174 |
-
# 1. Target start position is within range
|
| 175 |
-
# 2. Target end position is within or equal to end position
|
| 176 |
if start <= target_pos and target_end <= end + 1:
|
| 177 |
filtered_targets.append(target)
|
| 178 |
|
| 179 |
self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
|
| 180 |
|
| 181 |
-
# Get sequence for this region
|
| 182 |
-
sequence = self._get_sequence_for_position(chrom, start, end)
|
| 183 |
-
|
| 184 |
# Format results
|
| 185 |
for target in filtered_targets:
|
| 186 |
result = {
|
| 187 |
'feature_type': 'Position',
|
| 188 |
-
'chromosome':
|
| 189 |
-
'feature_id': position_name,
|
| 190 |
'feature_name': position_name,
|
| 191 |
-
'feature_description': position_name,
|
| 192 |
'location': target['location'],
|
| 193 |
'start': start,
|
| 194 |
'end': end,
|
|
@@ -196,15 +199,10 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 196 |
'sequence': target['sequence'],
|
| 197 |
'pam': target['pam'],
|
| 198 |
'score': target['score'],
|
| 199 |
-
'endonuclease': target['endonuclease']
|
| 200 |
-
'gene_sequence': sequence
|
| 201 |
}
|
| 202 |
all_results.append(result)
|
| 203 |
|
| 204 |
-
self.logger.debug(f"Added {len(filtered_targets)} formatted results")
|
| 205 |
-
else:
|
| 206 |
-
self.logger.warning(f"No targets found for query: {query}")
|
| 207 |
-
|
| 208 |
except Exception as e:
|
| 209 |
self.logger.error(f"Error processing query {query}: {str(e)}")
|
| 210 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
@@ -261,11 +259,6 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 261 |
try:
|
| 262 |
sequence = input_data['search_query'].strip().upper()
|
| 263 |
|
| 264 |
-
# Validate sequence length
|
| 265 |
-
if len(sequence) < 100:
|
| 266 |
-
self.logger.error("Sequence too short")
|
| 267 |
-
return []
|
| 268 |
-
|
| 269 |
# Get annotation file
|
| 270 |
annotation_file = self.global_settings.get_current_annotation_file()
|
| 271 |
if not annotation_file:
|
|
@@ -279,9 +272,7 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 279 |
self.annotation_parser.set_annotation_file(annotation_path)
|
| 280 |
|
| 281 |
# Find sequence in genome
|
| 282 |
-
chrom_count = 0
|
| 283 |
for record in SeqIO.parse(self.annotation_parser.annotation_file_name, "genbank"):
|
| 284 |
-
chrom_count += 1 # Count chromosome position by caret
|
| 285 |
record_seq = str(record.seq).upper()
|
| 286 |
pos = record_seq.find(sequence)
|
| 287 |
|
|
@@ -290,8 +281,8 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 290 |
start = pos + 1 # 1-based position
|
| 291 |
end = start + len(sequence) - 1
|
| 292 |
|
| 293 |
-
# Create position name
|
| 294 |
-
position_name = f"
|
| 295 |
|
| 296 |
# Create target info
|
| 297 |
target_info = [{
|
|
@@ -299,13 +290,13 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 299 |
'end': end,
|
| 300 |
'feature_id': position_name,
|
| 301 |
'feature_name': position_name,
|
| 302 |
-
'chromosome':
|
| 303 |
-
'
|
| 304 |
}]
|
| 305 |
|
| 306 |
# Get targets in this region
|
| 307 |
-
self.logger.debug(f"Found sequence in chromosome {
|
| 308 |
-
targets = parser.read_targets_batch(
|
| 309 |
|
| 310 |
if targets:
|
| 311 |
self.logger.debug(f"Found {len(targets)} raw targets")
|
|
@@ -322,15 +313,12 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 322 |
|
| 323 |
self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
|
| 324 |
|
| 325 |
-
# Get sequence with padding
|
| 326 |
-
sequence_with_padding = self._get_sequence_for_position(chrom_count, start, end)
|
| 327 |
-
|
| 328 |
# Format results
|
| 329 |
all_results = []
|
| 330 |
for target in filtered_targets:
|
| 331 |
result = {
|
| 332 |
'feature_type': 'Position',
|
| 333 |
-
'chromosome':
|
| 334 |
'feature_id': position_name,
|
| 335 |
'feature_name': position_name,
|
| 336 |
'feature_description': f"Sequence match at {position_name}",
|
|
@@ -341,8 +329,7 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 341 |
'sequence': target['sequence'],
|
| 342 |
'pam': target['pam'],
|
| 343 |
'score': target['score'],
|
| 344 |
-
'endonuclease': target['endonuclease']
|
| 345 |
-
'gene_sequence': sequence_with_padding
|
| 346 |
}
|
| 347 |
all_results.append(result)
|
| 348 |
|
|
|
|
|
|
|
| 1 |
from models.HomeWindowModel import HomeWindowModel
|
| 2 |
from models.CSPRparser import CSPRparser
|
| 3 |
from models.AnnotationParser import AnnotationParser
|
|
|
|
| 66 |
|
| 67 |
def find_targets_by_feature(self, parser, input_data):
|
| 68 |
try:
|
| 69 |
+
# Get annotation file with proper validation
|
| 70 |
+
annotation_file = input_data.get('annotation_file')
|
| 71 |
|
| 72 |
+
if not annotation_file:
|
| 73 |
+
self.global_settings.logger.error("No annotation file selected")
|
| 74 |
+
raise ValueError("No annotation file selected. Please select an annotation file.")
|
| 75 |
|
| 76 |
+
# Construct proper path
|
| 77 |
+
annotation_file_path = os.path.normpath(os.path.join(
|
| 78 |
+
self.global_settings.get_db_path(),
|
| 79 |
+
'GBFF',
|
| 80 |
+
annotation_file
|
| 81 |
+
))
|
| 82 |
|
| 83 |
+
if not os.path.isfile(annotation_file_path):
|
| 84 |
+
self.global_settings.logger.error(f"Annotation file not found: {annotation_file_path}")
|
| 85 |
+
raise FileNotFoundError(f"Annotation file not found: {annotation_file_path}")
|
| 86 |
|
| 87 |
+
self.global_settings.logger.debug(f"Using annotation file: {annotation_file_path}")
|
| 88 |
|
| 89 |
+
# Split search queries by newlines and remove empty lines
|
| 90 |
+
search_queries = [q.strip() for q in input_data['search_query'].split('\n') if q.strip()]
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
annotation_parser = AnnotationParser(self.global_settings)
|
| 93 |
+
annotation_parser.set_annotation_file(annotation_file_path)
|
| 94 |
+
|
| 95 |
+
# Process each query and combine results
|
| 96 |
+
all_results = []
|
| 97 |
+
for search_query in search_queries:
|
| 98 |
+
results_list = annotation_parser.genbank_search([search_query])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
for record_id, feature_info in results_list:
|
| 101 |
+
location = feature_info['feature_location']
|
| 102 |
+
start_end = location.split('(')[0]
|
| 103 |
+
start, end = map(int, start_end.split(':'))
|
| 104 |
+
|
| 105 |
+
target_info = {
|
| 106 |
+
'feature_type': feature_info['feature_type'],
|
| 107 |
+
'chromosome': record_id,
|
| 108 |
+
'feature_id': feature_info['feature_id'],
|
| 109 |
+
'feature_name': feature_info['feature_name'],
|
| 110 |
+
'feature_description': feature_info['feature_description'],
|
| 111 |
+
'location': f"{start}-{end}",
|
| 112 |
+
'full_location': feature_info.get('feature_full_location', ''),
|
| 113 |
+
'start': start,
|
| 114 |
+
'end': end,
|
| 115 |
+
'strand': '+' if '(+)' in location else '-',
|
| 116 |
+
'endonuclease': input_data['endonuclease'],
|
| 117 |
+
'search_query': search_query
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
all_results.append(target_info)
|
| 121 |
+
|
| 122 |
+
return all_results
|
| 123 |
|
| 124 |
except Exception as e:
|
| 125 |
self.global_settings.logger.error(f"Error in find_targets_by_feature: {str(e)}")
|
|
|
|
| 132 |
|
| 133 |
for query in queries:
|
| 134 |
try:
|
| 135 |
+
# Parse the position query
|
| 136 |
+
chrom_pos, start, end = map(int, query.strip().split(','))
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
# Get annotation file path
|
| 139 |
annotation_file = self.global_settings.get_current_annotation_file()
|
| 140 |
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 141 |
|
| 142 |
+
# Find the chromosome by its position in the file
|
| 143 |
+
full_chrom = None
|
| 144 |
+
chromosome_count = 0
|
| 145 |
+
|
| 146 |
for record in SeqIO.parse(annotation_path, "genbank"):
|
| 147 |
+
chromosome_count += 1
|
| 148 |
+
if chromosome_count == chrom_pos:
|
| 149 |
full_chrom = record.id
|
| 150 |
+
self.logger.debug(f"Found chromosome at position {chrom_pos} with ID {full_chrom}")
|
| 151 |
break
|
| 152 |
|
| 153 |
if not full_chrom:
|
| 154 |
+
self.logger.warning(f"Could not find chromosome at position {chrom_pos}. Total chromosomes: {chromosome_count}")
|
| 155 |
continue
|
| 156 |
|
| 157 |
+
# Create position name using full chromosome ID
|
| 158 |
+
position_name = f"chromosome {full_chrom}, start: {start}, end: {end}"
|
| 159 |
target_info = [{
|
| 160 |
'start': start,
|
| 161 |
'end': end,
|
| 162 |
+
'feature_id': position_name, # Use consistent format
|
| 163 |
'feature_name': position_name,
|
| 164 |
+
'chromosome': full_chrom, # Use raw chromosome ID
|
| 165 |
+
'feature_type': 'Position' # Add feature type
|
| 166 |
}]
|
| 167 |
|
| 168 |
# Get targets using batch processing
|
| 169 |
+
self.logger.debug(f"Searching for targets in chromosome {full_chrom} from {start} to {end}")
|
| 170 |
+
targets = parser.read_targets_batch(full_chrom, target_info, input_data['endonuclease'])
|
| 171 |
|
| 172 |
if targets:
|
| 173 |
self.logger.debug(f"Found {len(targets)} raw targets")
|
| 174 |
filtered_targets = []
|
| 175 |
+
guide_length = 23
|
| 176 |
|
| 177 |
for target in targets:
|
| 178 |
target_pos = int(target['position'])
|
| 179 |
+
target_end = target_pos + guide_length
|
| 180 |
|
| 181 |
+
# Include target if within sequence bounds
|
|
|
|
|
|
|
| 182 |
if start <= target_pos and target_end <= end + 1:
|
| 183 |
filtered_targets.append(target)
|
| 184 |
|
| 185 |
self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
|
| 186 |
|
|
|
|
|
|
|
|
|
|
| 187 |
# Format results
|
| 188 |
for target in filtered_targets:
|
| 189 |
result = {
|
| 190 |
'feature_type': 'Position',
|
| 191 |
+
'chromosome': full_chrom, # Use raw chromosome ID
|
| 192 |
+
'feature_id': position_name, # Use consistent format
|
| 193 |
'feature_name': position_name,
|
| 194 |
+
'feature_description': f"Position match at {position_name}",
|
| 195 |
'location': target['location'],
|
| 196 |
'start': start,
|
| 197 |
'end': end,
|
|
|
|
| 199 |
'sequence': target['sequence'],
|
| 200 |
'pam': target['pam'],
|
| 201 |
'score': target['score'],
|
| 202 |
+
'endonuclease': target['endonuclease']
|
|
|
|
| 203 |
}
|
| 204 |
all_results.append(result)
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
except Exception as e:
|
| 207 |
self.logger.error(f"Error processing query {query}: {str(e)}")
|
| 208 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
|
|
| 259 |
try:
|
| 260 |
sequence = input_data['search_query'].strip().upper()
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
# Get annotation file
|
| 263 |
annotation_file = self.global_settings.get_current_annotation_file()
|
| 264 |
if not annotation_file:
|
|
|
|
| 272 |
self.annotation_parser.set_annotation_file(annotation_path)
|
| 273 |
|
| 274 |
# Find sequence in genome
|
|
|
|
| 275 |
for record in SeqIO.parse(self.annotation_parser.annotation_file_name, "genbank"):
|
|
|
|
| 276 |
record_seq = str(record.seq).upper()
|
| 277 |
pos = record_seq.find(sequence)
|
| 278 |
|
|
|
|
| 281 |
start = pos + 1 # 1-based position
|
| 282 |
end = start + len(sequence) - 1
|
| 283 |
|
| 284 |
+
# Create position name using consistent format
|
| 285 |
+
position_name = f"chromosome {record.id}, start: {start}, end: {end}" # Match format used in position search
|
| 286 |
|
| 287 |
# Create target info
|
| 288 |
target_info = [{
|
|
|
|
| 290 |
'end': end,
|
| 291 |
'feature_id': position_name,
|
| 292 |
'feature_name': position_name,
|
| 293 |
+
'chromosome': record.id, # Use raw chromosome ID
|
| 294 |
+
'feature_type': 'Position'
|
| 295 |
}]
|
| 296 |
|
| 297 |
# Get targets in this region
|
| 298 |
+
self.logger.debug(f"Found sequence in chromosome {record.id} from {start} to {end}")
|
| 299 |
+
targets = parser.read_targets_batch(record.id, target_info, input_data['endonuclease'])
|
| 300 |
|
| 301 |
if targets:
|
| 302 |
self.logger.debug(f"Found {len(targets)} raw targets")
|
|
|
|
| 313 |
|
| 314 |
self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
|
| 315 |
|
|
|
|
|
|
|
|
|
|
| 316 |
# Format results
|
| 317 |
all_results = []
|
| 318 |
for target in filtered_targets:
|
| 319 |
result = {
|
| 320 |
'feature_type': 'Position',
|
| 321 |
+
'chromosome': record.id,
|
| 322 |
'feature_id': position_name,
|
| 323 |
'feature_name': position_name,
|
| 324 |
'feature_description': f"Sequence match at {position_name}",
|
|
|
|
| 329 |
'sequence': target['sequence'],
|
| 330 |
'pam': target['pam'],
|
| 331 |
'score': target['score'],
|
| 332 |
+
'endonuclease': target['endonuclease']
|
|
|
|
| 333 |
}
|
| 334 |
all_results.append(result)
|
| 335 |
|
|
@@ -4,13 +4,52 @@ import sys
|
|
| 4 |
import platform
|
| 5 |
from functools import lru_cache
|
| 6 |
import importlib
|
| 7 |
-
from PyQt6.QtCore import QSettings, QObject, pyqtSignal
|
| 8 |
from PyQt6.QtGui import QPalette, QColor
|
| 9 |
from PyQt6.QtWidgets import QApplication
|
|
|
|
| 10 |
|
| 11 |
from models.DatabaseManager import DatabaseManager, FileChangeType
|
| 12 |
from models.ConfigManager import ConfigManager
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
class GlobalSettings(QObject):
|
| 15 |
first_time_startup = pyqtSignal()
|
| 16 |
endonuclease_updated = pyqtSignal()
|
|
@@ -23,31 +62,91 @@ class GlobalSettings(QObject):
|
|
| 23 |
self.app_dir_path = app_dir_path
|
| 24 |
self.logger = self._setup_logging()
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
|
| 27 |
self.config_manager.load_env()
|
| 28 |
|
| 29 |
self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
|
| 30 |
|
|
|
|
| 31 |
self._initialize_directories()
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
def _on_db_files_changed(self, changes):
|
| 53 |
"""Handle database file changes"""
|
|
@@ -171,25 +270,6 @@ class GlobalSettings(QObject):
|
|
| 171 |
else:
|
| 172 |
app.setPalette(self.light_palette)
|
| 173 |
|
| 174 |
-
def initialize_palettes(self):
|
| 175 |
-
self.light_palette = QPalette() # Use default Qt light palette
|
| 176 |
-
self.dark_palette = QPalette()
|
| 177 |
-
|
| 178 |
-
# Set up dark palette
|
| 179 |
-
self.dark_palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
|
| 180 |
-
self.dark_palette.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
|
| 181 |
-
self.dark_palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
|
| 182 |
-
self.dark_palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
|
| 183 |
-
self.dark_palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(255, 255, 255))
|
| 184 |
-
self.dark_palette.setColor(QPalette.ColorRole.ToolTipText, QColor(255, 255, 255))
|
| 185 |
-
self.dark_palette.setColor(QPalette.ColorRole.Text, QColor(255, 255, 255))
|
| 186 |
-
self.dark_palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
|
| 187 |
-
self.dark_palette.setColor(QPalette.ColorRole.ButtonText, QColor(255, 255, 255))
|
| 188 |
-
self.dark_palette.setColor(QPalette.ColorRole.BrightText, QColor(255, 0, 0))
|
| 189 |
-
self.dark_palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
|
| 190 |
-
self.dark_palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
|
| 191 |
-
self.dark_palette.setColor(QPalette.ColorRole.HighlightedText, QColor(0, 0, 0))
|
| 192 |
-
|
| 193 |
def save_config(self):
|
| 194 |
self.config_manager.save_config()
|
| 195 |
|
|
@@ -213,58 +293,43 @@ class GlobalSettings(QObject):
|
|
| 213 |
|
| 214 |
@lru_cache(maxsize=None)
|
| 215 |
def _get_window_class(self, window_name):
|
| 216 |
-
"""Get the controller class with
|
| 217 |
try:
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
else:
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
-
if os.path.exists(
|
| 237 |
-
|
| 238 |
-
f"models.{model_name}",
|
| 239 |
-
model_file
|
| 240 |
-
)
|
| 241 |
-
model_module = importlib.util.module_from_spec(spec)
|
| 242 |
-
spec.loader.exec_module(model_module)
|
| 243 |
-
sys.modules[f"models.{model_name}"] = model_module
|
| 244 |
-
self.logger.debug(f"Successfully imported model from {model_file}")
|
| 245 |
-
except Exception as e:
|
| 246 |
-
self.logger.warning(f"Could not find model for {window_name}: {str(e)}")
|
| 247 |
-
|
| 248 |
-
# Import controller (required)
|
| 249 |
-
controller_name = f"{window_name}Controller"
|
| 250 |
-
controller_file = os.path.join(root_dir, 'controllers', f"{controller_name}.py")
|
| 251 |
-
|
| 252 |
-
if not os.path.exists(controller_file):
|
| 253 |
-
raise ImportError(f"Controller file not found: {controller_file}")
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
spec.loader.exec_module(controller_module)
|
| 261 |
-
sys.modules[f"controllers.{controller_name}"] = controller_module
|
| 262 |
|
| 263 |
class_name = f"{window_name}Controller"
|
| 264 |
if not hasattr(controller_module, class_name):
|
| 265 |
raise AttributeError(f"Controller module does not contain class {class_name}")
|
| 266 |
|
| 267 |
-
self.logger.debug(f"
|
| 268 |
return getattr(controller_module, class_name)
|
| 269 |
|
| 270 |
except Exception as e:
|
|
@@ -292,9 +357,21 @@ class GlobalSettings(QObject):
|
|
| 292 |
return self._startup_window
|
| 293 |
|
| 294 |
def get_home_window(self):
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
def get_new_genome_window(self):
|
| 300 |
controller = self._create_window("NewGenomeWindow")
|
|
@@ -311,10 +388,68 @@ class GlobalSettings(QObject):
|
|
| 311 |
self._current_ncbi_window = controller
|
| 312 |
return controller
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
def get_multitargeting_window(self):
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
def get_population_analysis_window(self):
|
| 320 |
controller = self._create_window("PopulationAnalysisWindow")
|
|
@@ -365,43 +500,36 @@ class GlobalSettings(QObject):
|
|
| 365 |
|
| 366 |
def set_current_annotation_file(self, annotation_file):
|
| 367 |
"""Set the current annotation file and notify listeners"""
|
| 368 |
-
|
| 369 |
-
self
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
|
| 373 |
def get_current_annotation_file(self):
|
| 374 |
"""Get the currently selected annotation file"""
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
def get_scoring_options_window(self, view_targets_controller):
|
| 381 |
"""Create and return ScoringOptionsController instance"""
|
| 382 |
from controllers.ScoringOptionsController import ScoringOptionsController
|
| 383 |
return ScoringOptionsController(self, view_targets_controller)
|
| 384 |
|
| 385 |
-
def get_stylesheet(self):
|
| 386 |
-
"""Return the base stylesheet for the application"""
|
| 387 |
-
# Implement this method to return a base stylesheet
|
| 388 |
-
pass
|
| 389 |
-
|
| 390 |
-
def get_groupbox_style(self):
|
| 391 |
-
"""Return the style for group boxes"""
|
| 392 |
-
# Implement this method to return the group box style
|
| 393 |
-
pass
|
| 394 |
-
|
| 395 |
-
def get_dark_stylesheet(self):
|
| 396 |
-
"""Return the dark theme stylesheet"""
|
| 397 |
-
# Implement this method to return the dark theme stylesheet
|
| 398 |
-
pass
|
| 399 |
-
|
| 400 |
-
def get_light_stylesheet(self):
|
| 401 |
-
"""Return the light theme stylesheet"""
|
| 402 |
-
# Implement this method to return the light theme stylesheet
|
| 403 |
-
pass
|
| 404 |
-
|
| 405 |
def set_theme(self, theme):
|
| 406 |
"""Set the current theme and notify listeners"""
|
| 407 |
self.theme = theme
|
|
@@ -425,5 +553,27 @@ class GlobalSettings(QObject):
|
|
| 425 |
self._export_selected_grnas_controller = ExportSelectedgRNAsController(self)
|
| 426 |
return self._export_selected_grnas_controller
|
| 427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
# Global instance
|
| 429 |
global_settings = None
|
|
|
|
| 4 |
import platform
|
| 5 |
from functools import lru_cache
|
| 6 |
import importlib
|
| 7 |
+
from PyQt6.QtCore import QSettings, QObject, pyqtSignal, QThread
|
| 8 |
from PyQt6.QtGui import QPalette, QColor
|
| 9 |
from PyQt6.QtWidgets import QApplication
|
| 10 |
+
import time
|
| 11 |
|
| 12 |
from models.DatabaseManager import DatabaseManager, FileChangeType
|
| 13 |
from models.ConfigManager import ConfigManager
|
| 14 |
|
| 15 |
+
class ModulePreloader(QThread):
|
| 16 |
+
finished = pyqtSignal(str, object)
|
| 17 |
+
|
| 18 |
+
def __init__(self, global_settings, module_name):
|
| 19 |
+
super().__init__()
|
| 20 |
+
self.global_settings = global_settings
|
| 21 |
+
self.module_name = module_name
|
| 22 |
+
self.module = None # Store the loaded module
|
| 23 |
+
|
| 24 |
+
def run(self):
|
| 25 |
+
try:
|
| 26 |
+
module_path = f"controllers.{self.module_name}Controller"
|
| 27 |
+
if module_path not in self.global_settings._module_cache:
|
| 28 |
+
# Get root directory
|
| 29 |
+
if hasattr(sys, 'frozen'):
|
| 30 |
+
root_dir = os.path.join(os.path.dirname(sys.executable), 'src')
|
| 31 |
+
if platform.system() == 'Darwin':
|
| 32 |
+
root_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(sys.executable))),
|
| 33 |
+
'Contents', 'Resources', 'src')
|
| 34 |
+
else:
|
| 35 |
+
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 36 |
+
|
| 37 |
+
if root_dir not in sys.path:
|
| 38 |
+
sys.path.insert(0, root_dir)
|
| 39 |
+
|
| 40 |
+
controller_file = os.path.join(root_dir, 'controllers', f"{self.module_name}Controller.py")
|
| 41 |
+
|
| 42 |
+
if os.path.exists(controller_file):
|
| 43 |
+
spec = importlib.util.spec_from_file_location(module_path, controller_file)
|
| 44 |
+
module = importlib.util.module_from_spec(spec)
|
| 45 |
+
spec.loader.exec_module(module)
|
| 46 |
+
sys.modules[module_path] = module
|
| 47 |
+
self.module = module
|
| 48 |
+
self.finished.emit(self.module_name, module)
|
| 49 |
+
|
| 50 |
+
except Exception as e:
|
| 51 |
+
self.global_settings.logger.error(f"Error preloading module {self.module_name}: {str(e)}")
|
| 52 |
+
|
| 53 |
class GlobalSettings(QObject):
|
| 54 |
first_time_startup = pyqtSignal()
|
| 55 |
endonuclease_updated = pyqtSignal()
|
|
|
|
| 62 |
self.app_dir_path = app_dir_path
|
| 63 |
self.logger = self._setup_logging()
|
| 64 |
|
| 65 |
+
# Initialize important attributes
|
| 66 |
+
self._current_annotation_file = None
|
| 67 |
+
self._module_cache = {}
|
| 68 |
+
self._preloading_modules = {}
|
| 69 |
+
self.main_window = None
|
| 70 |
+
|
| 71 |
+
# Only preload essential controllers for startup
|
| 72 |
+
self._preload_essential_controllers()
|
| 73 |
+
|
| 74 |
+
# Start background loading of commonly used modules
|
| 75 |
+
self._background_load_common_modules()
|
| 76 |
+
|
| 77 |
self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
|
| 78 |
self.config_manager.load_env()
|
| 79 |
|
| 80 |
self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
|
| 81 |
|
| 82 |
+
# Defer database initialization until needed
|
| 83 |
self._initialize_directories()
|
| 84 |
+
self._init_db_manager()
|
| 85 |
|
| 86 |
+
# Defer theme initialization
|
| 87 |
+
self._init_theme_settings()
|
| 88 |
+
|
| 89 |
+
def _init_db_manager(self):
|
| 90 |
+
"""Initialize database manager lazily"""
|
| 91 |
+
if not hasattr(self, 'db_manager'):
|
| 92 |
+
self.db_manager = DatabaseManager(self.logger, self.config_manager)
|
| 93 |
+
self.db_manager.db_files_changed.connect(self._on_db_files_changed)
|
| 94 |
+
self.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
|
| 95 |
+
self.db_manager.db_state_changed.connect(self._on_db_state_changed)
|
| 96 |
+
self.CSPR_DB = self.db_manager.get_db_path()
|
| 97 |
+
self.algorithms = self.config_manager.get_config_value('algorithms', ["Azimuth 2.0"])
|
| 98 |
+
|
| 99 |
+
def _init_theme_settings(self):
|
| 100 |
+
"""Initialize theme settings lazily"""
|
| 101 |
+
if not hasattr(self, 'settings'):
|
| 102 |
+
self.settings = QSettings("TrinhLab-UTK", "CASPER")
|
| 103 |
+
self.theme = self.settings.value("theme", "light")
|
| 104 |
+
self.light_palette = None
|
| 105 |
+
self.dark_palette = None
|
| 106 |
+
|
| 107 |
+
def _preload_essential_controllers(self):
|
| 108 |
+
"""Preload only the essential controllers needed for startup"""
|
| 109 |
+
try:
|
| 110 |
+
essential_controllers = [
|
| 111 |
+
"StartupWindow",
|
| 112 |
+
"HomeWindow"
|
| 113 |
+
] if self.is_first_time_startup else ["HomeWindow"]
|
| 114 |
+
|
| 115 |
+
for controller_name in essential_controllers:
|
| 116 |
+
self._preload_controller(controller_name)
|
| 117 |
+
|
| 118 |
+
except Exception as e:
|
| 119 |
+
self.logger.warning(f"Essential controller preloading failed: {str(e)}")
|
| 120 |
|
| 121 |
+
def _preload_controller(self, window_name):
|
| 122 |
+
"""Preload a single controller with optimized imports"""
|
| 123 |
+
try:
|
| 124 |
+
module_path = f"controllers.{window_name}Controller"
|
| 125 |
+
if module_path not in self._module_cache:
|
| 126 |
+
# Get root directory only once
|
| 127 |
+
if not hasattr(self, '_root_dir'):
|
| 128 |
+
if hasattr(sys, 'frozen'):
|
| 129 |
+
self._root_dir = os.path.join(os.path.dirname(sys.executable), 'src')
|
| 130 |
+
if platform.system() == 'Darwin':
|
| 131 |
+
self._root_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(sys.executable))),
|
| 132 |
+
'Contents', 'Resources', 'src')
|
| 133 |
+
else:
|
| 134 |
+
self._root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 135 |
+
|
| 136 |
+
if self._root_dir not in sys.path:
|
| 137 |
+
sys.path.insert(0, self._root_dir)
|
| 138 |
+
|
| 139 |
+
controller_file = os.path.join(self._root_dir, 'controllers', f"{window_name}Controller.py")
|
| 140 |
+
|
| 141 |
+
if os.path.exists(controller_file):
|
| 142 |
+
spec = importlib.util.spec_from_file_location(module_path, controller_file)
|
| 143 |
+
module = importlib.util.module_from_spec(spec)
|
| 144 |
+
spec.loader.exec_module(module)
|
| 145 |
+
sys.modules[module_path] = module
|
| 146 |
+
self._module_cache[module_path] = module
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
self.logger.warning(f"Failed to preload controller {window_name}: {str(e)}")
|
| 150 |
|
| 151 |
def _on_db_files_changed(self, changes):
|
| 152 |
"""Handle database file changes"""
|
|
|
|
| 270 |
else:
|
| 271 |
app.setPalette(self.light_palette)
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
def save_config(self):
|
| 274 |
self.config_manager.save_config()
|
| 275 |
|
|
|
|
| 293 |
|
| 294 |
@lru_cache(maxsize=None)
|
| 295 |
def _get_window_class(self, window_name):
|
| 296 |
+
"""Get the controller class with optimized loading"""
|
| 297 |
try:
|
| 298 |
+
start_time = time.time()
|
| 299 |
+
|
| 300 |
+
# Check if module is already cached
|
| 301 |
+
module_path = f"controllers.{window_name}Controller"
|
| 302 |
+
if module_path in self._module_cache:
|
| 303 |
+
controller_module = self._module_cache[module_path]
|
| 304 |
else:
|
| 305 |
+
# Fall back to regular import if not cached
|
| 306 |
+
if hasattr(sys, 'frozen'):
|
| 307 |
+
root_dir = os.path.join(os.path.dirname(sys.executable), 'src')
|
| 308 |
+
if platform.system() == 'Darwin':
|
| 309 |
+
root_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(sys.executable))),
|
| 310 |
+
'Contents', 'Resources', 'src')
|
| 311 |
+
else:
|
| 312 |
+
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 313 |
+
|
| 314 |
+
if root_dir not in sys.path:
|
| 315 |
+
sys.path.insert(0, root_dir)
|
| 316 |
+
|
| 317 |
+
controller_file = os.path.join(root_dir, 'controllers', f"{window_name}Controller.py")
|
| 318 |
|
| 319 |
+
if not os.path.exists(controller_file):
|
| 320 |
+
raise ImportError(f"Controller file not found: {controller_file}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
+
spec = importlib.util.spec_from_file_location(module_path, controller_file)
|
| 323 |
+
controller_module = importlib.util.module_from_spec(spec)
|
| 324 |
+
spec.loader.exec_module(controller_module)
|
| 325 |
+
sys.modules[module_path] = controller_module
|
| 326 |
+
self._module_cache[module_path] = controller_module
|
|
|
|
|
|
|
| 327 |
|
| 328 |
class_name = f"{window_name}Controller"
|
| 329 |
if not hasattr(controller_module, class_name):
|
| 330 |
raise AttributeError(f"Controller module does not contain class {class_name}")
|
| 331 |
|
| 332 |
+
self.logger.debug(f"Window class retrieval took: {time.time() - start_time:.2f} seconds")
|
| 333 |
return getattr(controller_module, class_name)
|
| 334 |
|
| 335 |
except Exception as e:
|
|
|
|
| 357 |
return self._startup_window
|
| 358 |
|
| 359 |
def get_home_window(self):
|
| 360 |
+
"""Get or create home window with proper initialization"""
|
| 361 |
+
try:
|
| 362 |
+
controller = self._create_window("HomeWindow")
|
| 363 |
+
self._current_home_window = controller
|
| 364 |
+
|
| 365 |
+
# Initialize annotation file if needed
|
| 366 |
+
if not hasattr(self, '_current_annotation_file'):
|
| 367 |
+
self._current_annotation_file = None
|
| 368 |
+
if hasattr(controller, 'view'):
|
| 369 |
+
self._current_annotation_file = controller.view.get_annotation_file()
|
| 370 |
+
|
| 371 |
+
return controller
|
| 372 |
+
except Exception as e:
|
| 373 |
+
self.logger.error(f"Error creating home window: {str(e)}")
|
| 374 |
+
raise
|
| 375 |
|
| 376 |
def get_new_genome_window(self):
|
| 377 |
controller = self._create_window("NewGenomeWindow")
|
|
|
|
| 388 |
self._current_ncbi_window = controller
|
| 389 |
return controller
|
| 390 |
|
| 391 |
+
def _background_load_common_modules(self):
|
| 392 |
+
"""Start background loading of commonly used modules"""
|
| 393 |
+
try:
|
| 394 |
+
common_modules = ["MultitargetingWindow", "PopulationAnalysisWindow"]
|
| 395 |
+
for module_name in common_modules:
|
| 396 |
+
if (module_name not in self._module_cache and
|
| 397 |
+
module_name not in self._preloading_modules):
|
| 398 |
+
preloader = ModulePreloader(self, module_name)
|
| 399 |
+
preloader.finished.connect(self._on_module_preloaded)
|
| 400 |
+
self._preloading_modules[module_name] = preloader
|
| 401 |
+
preloader.start()
|
| 402 |
+
except Exception as e:
|
| 403 |
+
self.logger.warning(f"Error starting background module loading: {str(e)}")
|
| 404 |
+
|
| 405 |
+
def _on_module_preloaded(self, module_name, module):
|
| 406 |
+
"""Handle completion of module preloading"""
|
| 407 |
+
try:
|
| 408 |
+
module_path = f"controllers.{module_name}Controller"
|
| 409 |
+
self._module_cache[module_path] = module
|
| 410 |
+
if module_name in self._preloading_modules:
|
| 411 |
+
preloader = self._preloading_modules[module_name]
|
| 412 |
+
if not preloader.isRunning(): # Only remove if thread is finished
|
| 413 |
+
del self._preloading_modules[module_name]
|
| 414 |
+
self.logger.debug(f"Module {module_name} preloaded successfully")
|
| 415 |
+
except Exception as e:
|
| 416 |
+
self.logger.error(f"Error handling preloaded module: {str(e)}")
|
| 417 |
+
|
| 418 |
def get_multitargeting_window(self):
|
| 419 |
+
"""Create and return MultitargetingController instance with optimized loading"""
|
| 420 |
+
try:
|
| 421 |
+
start_time = time.time()
|
| 422 |
+
self.logger.debug("Starting multitargeting window creation")
|
| 423 |
+
|
| 424 |
+
# Check if module is being preloaded
|
| 425 |
+
if "MultitargetingWindow" in self._preloading_modules:
|
| 426 |
+
preloader = self._preloading_modules["MultitargetingWindow"]
|
| 427 |
+
if preloader.isRunning():
|
| 428 |
+
self.logger.debug("Waiting for preloader to complete...")
|
| 429 |
+
preloader.wait()
|
| 430 |
+
if preloader.module: # Use the stored module
|
| 431 |
+
WindowClass = getattr(preloader.module, "MultitargetingWindowController")
|
| 432 |
+
else:
|
| 433 |
+
WindowClass = self._get_window_class("MultitargetingWindow")
|
| 434 |
+
else:
|
| 435 |
+
WindowClass = self._get_window_class("MultitargetingWindow")
|
| 436 |
+
else:
|
| 437 |
+
WindowClass = self._get_window_class("MultitargetingWindow")
|
| 438 |
+
|
| 439 |
+
# Create controller instance
|
| 440 |
+
controller_start = time.time()
|
| 441 |
+
controller = WindowClass(self)
|
| 442 |
+
self.logger.debug(f"Controller instantiation took: {time.time() - controller_start:.2f} seconds")
|
| 443 |
+
|
| 444 |
+
# Store the reference
|
| 445 |
+
self._current_multitargeting_window = controller
|
| 446 |
+
|
| 447 |
+
self.logger.debug(f"Total multitargeting window creation took: {time.time() - start_time:.2f} seconds")
|
| 448 |
+
return controller
|
| 449 |
+
|
| 450 |
+
except Exception as e:
|
| 451 |
+
self.logger.error(f"Error creating multitargeting window: {str(e)}")
|
| 452 |
+
raise
|
| 453 |
|
| 454 |
def get_population_analysis_window(self):
|
| 455 |
controller = self._create_window("PopulationAnalysisWindow")
|
|
|
|
| 500 |
|
| 501 |
def set_current_annotation_file(self, annotation_file):
|
| 502 |
"""Set the current annotation file and notify listeners"""
|
| 503 |
+
try:
|
| 504 |
+
if not hasattr(self, '_current_annotation_file'):
|
| 505 |
+
self._current_annotation_file = None
|
| 506 |
+
|
| 507 |
+
if self._current_annotation_file != annotation_file:
|
| 508 |
+
self._current_annotation_file = annotation_file
|
| 509 |
+
self.logger.debug(f"Current annotation file changed to: {annotation_file}")
|
| 510 |
+
self.annotation_file_changed.emit(annotation_file)
|
| 511 |
+
except Exception as e:
|
| 512 |
+
self.logger.error(f"Error setting current annotation file: {str(e)}")
|
| 513 |
|
| 514 |
def get_current_annotation_file(self):
|
| 515 |
"""Get the currently selected annotation file"""
|
| 516 |
+
try:
|
| 517 |
+
if not self._current_annotation_file and hasattr(self, '_current_home_window'):
|
| 518 |
+
# Try to get from home window if not set
|
| 519 |
+
home_controller = self._current_home_window
|
| 520 |
+
if hasattr(home_controller, 'view'):
|
| 521 |
+
self._current_annotation_file = home_controller.view.get_annotation_file()
|
| 522 |
+
self.logger.debug(f"Got annotation file from home window: {self._current_annotation_file}")
|
| 523 |
+
return self._current_annotation_file
|
| 524 |
+
except Exception as e:
|
| 525 |
+
self.logger.error(f"Error getting current annotation file: {str(e)}")
|
| 526 |
+
return None
|
| 527 |
|
| 528 |
def get_scoring_options_window(self, view_targets_controller):
|
| 529 |
"""Create and return ScoringOptionsController instance"""
|
| 530 |
from controllers.ScoringOptionsController import ScoringOptionsController
|
| 531 |
return ScoringOptionsController(self, view_targets_controller)
|
| 532 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
def set_theme(self, theme):
|
| 534 |
"""Set the current theme and notify listeners"""
|
| 535 |
self.theme = theme
|
|
|
|
| 553 |
self._export_selected_grnas_controller = ExportSelectedgRNAsController(self)
|
| 554 |
return self._export_selected_grnas_controller
|
| 555 |
|
| 556 |
+
def adjust_path_for_os(self, path):
|
| 557 |
+
"""
|
| 558 |
+
Adjust file path based on operating system
|
| 559 |
+
"""
|
| 560 |
+
try:
|
| 561 |
+
# Convert path separators to match the current OS
|
| 562 |
+
adjusted_path = os.path.normpath(path)
|
| 563 |
+
|
| 564 |
+
# For Windows, ensure the path uses backslashes
|
| 565 |
+
if platform.system() == 'Windows':
|
| 566 |
+
adjusted_path = adjusted_path.replace('/', '\\')
|
| 567 |
+
# For Unix-like systems (Linux, macOS), ensure the path uses forward slashes
|
| 568 |
+
else:
|
| 569 |
+
adjusted_path = adjusted_path.replace('\\', '/')
|
| 570 |
+
|
| 571 |
+
self.logger.debug(f"Adjusted path from '{path}' to '{adjusted_path}'")
|
| 572 |
+
return adjusted_path
|
| 573 |
+
|
| 574 |
+
except Exception as e:
|
| 575 |
+
self.logger.error(f"Error adjusting path: {str(e)}")
|
| 576 |
+
return path # Return original path if adjustment fails
|
| 577 |
+
|
| 578 |
# Global instance
|
| 579 |
global_settings = None
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
import glob
|
| 3 |
-
from typing import Dict, List
|
| 4 |
from utils.ui import show_error
|
| 5 |
from models.DatabaseManager import FileChangeType
|
| 6 |
|
|
|
|
| 1 |
import os
|
| 2 |
import glob
|
| 3 |
+
from typing import Dict, List
|
| 4 |
from utils.ui import show_error
|
| 5 |
from models.DatabaseManager import FileChangeType
|
| 6 |
|
|
@@ -1,9 +1,12 @@
|
|
| 1 |
import os
|
| 2 |
import sqlite3
|
| 3 |
import statistics
|
|
|
|
|
|
|
| 4 |
|
| 5 |
class MultitargetingWindowModel:
|
| 6 |
def __init__(self, global_settings):
|
|
|
|
| 7 |
self.settings = global_settings
|
| 8 |
self.logger = global_settings.get_logger()
|
| 9 |
|
|
@@ -12,14 +15,21 @@ class MultitargetingWindowModel:
|
|
| 12 |
self.row_limit = 1000
|
| 13 |
|
| 14 |
# Get organism and endo mappings from DatabaseManager
|
|
|
|
| 15 |
self.organisms_to_files, self.organisms_to_endos = self.settings.db_manager.get_organisms_and_endos()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
|
|
|
| 17 |
def get_organisms(self):
|
| 18 |
-
"""Get list of available organisms"""
|
| 19 |
return list(self.organisms_to_endos.keys())
|
| 20 |
|
|
|
|
| 21 |
def get_endos_for_organism(self, organism):
|
| 22 |
-
"""Get available endonucleases for given organism"""
|
| 23 |
return self.organisms_to_endos.get(organism, [])
|
| 24 |
|
| 25 |
def set_files(self, organism, endo):
|
|
@@ -36,6 +46,7 @@ class MultitargetingWindowModel:
|
|
| 36 |
|
| 37 |
def get_repeats_data(self):
|
| 38 |
"""Get repeats data for the seeds table"""
|
|
|
|
| 39 |
if not self.db_file:
|
| 40 |
raise ValueError("Database file not set. Please select an organism and endonuclease first.")
|
| 41 |
|
|
@@ -43,6 +54,7 @@ class MultitargetingWindowModel:
|
|
| 43 |
conn = sqlite3.connect(self.db_file)
|
| 44 |
c = conn.cursor()
|
| 45 |
|
|
|
|
| 46 |
# Use row limit in query
|
| 47 |
if self.row_limit == -1: # No limit
|
| 48 |
query = "SELECT * FROM repeats ORDER BY count DESC;"
|
|
@@ -107,9 +119,13 @@ class MultitargetingWindowModel:
|
|
| 107 |
pams[majority_index], # PAM
|
| 108 |
strand # Strand
|
| 109 |
))
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
c.close()
|
| 112 |
conn.close()
|
|
|
|
|
|
|
| 113 |
return results
|
| 114 |
|
| 115 |
except Exception as e:
|
|
@@ -280,3 +296,9 @@ class MultitargetingWindowModel:
|
|
| 280 |
def get_row_limit(self):
|
| 281 |
"""Get current row limit setting"""
|
| 282 |
return self.row_limit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import sqlite3
|
| 3 |
import statistics
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
import time
|
| 6 |
|
| 7 |
class MultitargetingWindowModel:
|
| 8 |
def __init__(self, global_settings):
|
| 9 |
+
start_time = time.time()
|
| 10 |
self.settings = global_settings
|
| 11 |
self.logger = global_settings.get_logger()
|
| 12 |
|
|
|
|
| 15 |
self.row_limit = 1000
|
| 16 |
|
| 17 |
# Get organism and endo mappings from DatabaseManager
|
| 18 |
+
db_start = time.time()
|
| 19 |
self.organisms_to_files, self.organisms_to_endos = self.settings.db_manager.get_organisms_and_endos()
|
| 20 |
+
self.logger.debug(f"Getting DB mappings took: {time.time() - db_start:.2f} seconds")
|
| 21 |
+
|
| 22 |
+
self._cache = {}
|
| 23 |
+
self.logger.debug(f"Model initialization took: {time.time() - start_time:.2f} seconds")
|
| 24 |
|
| 25 |
+
@lru_cache(maxsize=32)
|
| 26 |
def get_organisms(self):
|
| 27 |
+
"""Get list of available organisms with caching"""
|
| 28 |
return list(self.organisms_to_endos.keys())
|
| 29 |
|
| 30 |
+
@lru_cache(maxsize=32)
|
| 31 |
def get_endos_for_organism(self, organism):
|
| 32 |
+
"""Get available endonucleases for given organism with caching"""
|
| 33 |
return self.organisms_to_endos.get(organism, [])
|
| 34 |
|
| 35 |
def set_files(self, organism, endo):
|
|
|
|
| 46 |
|
| 47 |
def get_repeats_data(self):
|
| 48 |
"""Get repeats data for the seeds table"""
|
| 49 |
+
start_time = time.time()
|
| 50 |
if not self.db_file:
|
| 51 |
raise ValueError("Database file not set. Please select an organism and endonuclease first.")
|
| 52 |
|
|
|
|
| 54 |
conn = sqlite3.connect(self.db_file)
|
| 55 |
c = conn.cursor()
|
| 56 |
|
| 57 |
+
query_start = time.time()
|
| 58 |
# Use row limit in query
|
| 59 |
if self.row_limit == -1: # No limit
|
| 60 |
query = "SELECT * FROM repeats ORDER BY count DESC;"
|
|
|
|
| 119 |
pams[majority_index], # PAM
|
| 120 |
strand # Strand
|
| 121 |
))
|
| 122 |
+
|
| 123 |
+
self.logger.debug(f"Query and processing took: {time.time() - query_start:.2f} seconds")
|
| 124 |
+
|
| 125 |
c.close()
|
| 126 |
conn.close()
|
| 127 |
+
|
| 128 |
+
self.logger.debug(f"Total get_repeats_data took: {time.time() - start_time:.2f} seconds")
|
| 129 |
return results
|
| 130 |
|
| 131 |
except Exception as e:
|
|
|
|
| 296 |
def get_row_limit(self):
|
| 297 |
"""Get current row limit setting"""
|
| 298 |
return self.row_limit
|
| 299 |
+
|
| 300 |
+
def _clear_cache(self):
|
| 301 |
+
"""Clear the internal cache"""
|
| 302 |
+
self._cache.clear()
|
| 303 |
+
self.get_organisms.cache_clear()
|
| 304 |
+
self.get_endos_for_organism.cache_clear()
|
|
@@ -1,42 +1,231 @@
|
|
| 1 |
import pandas as pd
|
| 2 |
from PyQt6 import QtCore, QtGui
|
| 3 |
from Bio import Entrez
|
| 4 |
-
from bs4 import BeautifulSoup, XMLParsedAsHTMLWarning
|
| 5 |
from ftplib import FTP
|
|
|
|
| 6 |
import gzip
|
| 7 |
import os
|
| 8 |
-
import ssl
|
| 9 |
import platform
|
| 10 |
-
import
|
| 11 |
-
import xml.etree.ElementTree as ET
|
| 12 |
-
from PyQt6.QtCore import Qt
|
| 13 |
-
import socket
|
| 14 |
from urllib.parse import urlparse
|
| 15 |
|
| 16 |
class NCBIWindowModel:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
def __init__(self, settings):
|
| 18 |
self.settings = settings
|
| 19 |
self.logger = settings.get_logger()
|
| 20 |
self.df = pd.DataFrame()
|
| 21 |
self.genbank_ftp_dict = {}
|
| 22 |
self.refseq_ftp_dict = {}
|
| 23 |
-
self.files = []
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
def search_ncbi(self, search_params):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
try:
|
| 34 |
retmax = int(search_params['max_results']) if search_params['max_results'] else 100
|
| 35 |
term = f'"{search_params["organism"]}"[Organism]'
|
| 36 |
|
| 37 |
if search_params['complete_genomes_only']:
|
| 38 |
term += ' AND "Complete Genome"[Assembly Level]'
|
| 39 |
-
if search_params['strain']:
|
| 40 |
term += f' AND "{search_params["strain"]}"[Infraspecific name]'
|
| 41 |
|
| 42 |
self.logger.info(f"Searching NCBI with term: {term}")
|
|
@@ -72,27 +261,176 @@ class NCBIWindowModel:
|
|
| 72 |
self.logger.error(f"Error in query_ncbi: {str(e)}", exc_info=True)
|
| 73 |
raise
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
def _process_ncbi_data(self, summary_results):
|
| 76 |
data = []
|
| 77 |
for assembly in summary_results['DocumentSummarySet']['DocumentSummary']:
|
| 78 |
gb_ftp = assembly.get('FtpPath_GenBank', '')
|
| 79 |
rs_ftp = assembly.get('FtpPath_RefSeq', '')
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
entry = {
|
| 82 |
-
'ID':
|
| 83 |
'Species Name': assembly.get('SpeciesName', 'N/A'),
|
| 84 |
-
'Strain':
|
| 85 |
'Assembly Name': assembly.get('AssemblyName', 'N/A'),
|
| 86 |
-
'RefSeq assembly accession':
|
| 87 |
-
'GenBank assembly accession':
|
| 88 |
'Assembly Status': assembly.get('AssemblyStatus', 'N/A')
|
| 89 |
}
|
| 90 |
|
|
|
|
| 91 |
if gb_ftp:
|
| 92 |
-
self.genbank_ftp_dict[
|
|
|
|
|
|
|
| 93 |
if rs_ftp:
|
| 94 |
-
self.refseq_ftp_dict[
|
| 95 |
-
|
|
|
|
| 96 |
data.append(entry)
|
| 97 |
|
| 98 |
self.df = pd.DataFrame(data)
|
|
@@ -102,20 +440,176 @@ class NCBIWindowModel:
|
|
| 102 |
self.logger.warning("No data found in NCBI response")
|
| 103 |
else:
|
| 104 |
self.logger.info(f"Processed {len(self.df)} entries from NCBI response")
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
if not self.df.empty:
|
| 108 |
-
self.logger.debug(f"Sample of processed data:\n{self.df.head().to_string()}")
|
| 109 |
|
| 110 |
def _get_element_text(self, element, tag):
|
| 111 |
el = element.find(tag)
|
| 112 |
return el.text if el is not None else 'N/A'
|
| 113 |
|
| 114 |
def get_download_url(self, id, use_genbank):
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
def decompress_file(self, filename):
|
| 121 |
try:
|
|
@@ -134,10 +628,9 @@ class NCBIWindowModel:
|
|
| 134 |
raise
|
| 135 |
|
| 136 |
def get_output_path(self, file_type):
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
return os.path.join(self.settings.CSPR_DB, file_type)
|
| 141 |
|
| 142 |
def rename_file(self, old_name, new_name, file_type):
|
| 143 |
old_path = os.path.join(self.get_output_path(file_type), old_name)
|
|
@@ -235,118 +728,3 @@ class CustomProxyModel(QtCore.QSortFilterProxyModel):
|
|
| 235 |
if regex.match(text).hasMatch():
|
| 236 |
return False
|
| 237 |
return True
|
| 238 |
-
|
| 239 |
-
class DownloadThread(QtCore.QThread):
|
| 240 |
-
finished = QtCore.pyqtSignal(bool)
|
| 241 |
-
progress_updated = QtCore.pyqtSignal(int, int, int)
|
| 242 |
-
status_updated = QtCore.pyqtSignal(str)
|
| 243 |
-
all_completed = QtCore.pyqtSignal()
|
| 244 |
-
|
| 245 |
-
def __init__(self, controller, url, id, species_name, strain, download_fna, download_gbff):
|
| 246 |
-
super().__init__()
|
| 247 |
-
self.controller = controller
|
| 248 |
-
self.url = url
|
| 249 |
-
self.id = id
|
| 250 |
-
self.species_name = species_name
|
| 251 |
-
self.strain = strain
|
| 252 |
-
self.download_fna = download_fna
|
| 253 |
-
self.download_gbff = download_gbff
|
| 254 |
-
|
| 255 |
-
def run(self):
|
| 256 |
-
try:
|
| 257 |
-
parsed_url = urlparse(self.url)
|
| 258 |
-
ftp_host = parsed_url.netloc
|
| 259 |
-
ftp_path = parsed_url.path
|
| 260 |
-
|
| 261 |
-
self.controller.logger.info(f"Attempting to connect to FTP server: {ftp_host}")
|
| 262 |
-
|
| 263 |
-
try:
|
| 264 |
-
ip_address = socket.gethostbyname(ftp_host)
|
| 265 |
-
self.controller.logger.info(f"Resolved IP address: {ip_address}")
|
| 266 |
-
except socket.gaierror as e:
|
| 267 |
-
self.controller.logger.error(f"Failed to resolve hostname: {ftp_host}. Error: {str(e)}")
|
| 268 |
-
self.finished.emit(False)
|
| 269 |
-
return
|
| 270 |
-
|
| 271 |
-
ftp = FTP(ftp_host)
|
| 272 |
-
ftp.login()
|
| 273 |
-
ftp.cwd(ftp_path)
|
| 274 |
-
ftp.set_pasv(True)
|
| 275 |
-
|
| 276 |
-
# Set binary mode before any operations
|
| 277 |
-
ftp.voidcmd('TYPE I')
|
| 278 |
-
|
| 279 |
-
files_to_download = []
|
| 280 |
-
|
| 281 |
-
# Get list of all files
|
| 282 |
-
all_files = ftp.nlst()
|
| 283 |
-
|
| 284 |
-
# Process FNA files if requested
|
| 285 |
-
if self.download_fna:
|
| 286 |
-
# Find the main genomic FNA file (should be exactly one)
|
| 287 |
-
genomic_fna = [f for f in all_files
|
| 288 |
-
if f.endswith('_genomic.fna.gz')
|
| 289 |
-
and not any(x in f for x in ['cds_from', 'rna_from'])]
|
| 290 |
-
|
| 291 |
-
if genomic_fna:
|
| 292 |
-
files_to_download.append(genomic_fna[0])
|
| 293 |
-
self.controller.logger.info(f"Found main genomic FNA file: {genomic_fna[0]}")
|
| 294 |
-
|
| 295 |
-
# Process GBFF files if requested
|
| 296 |
-
if self.download_gbff:
|
| 297 |
-
gbff_files = [f for f in all_files if f.endswith('_genomic.gbff.gz')]
|
| 298 |
-
files_to_download.extend(gbff_files)
|
| 299 |
-
self.controller.logger.info(f"Found GBFF files: {gbff_files}")
|
| 300 |
-
|
| 301 |
-
# Calculate total size with error handling
|
| 302 |
-
total_size = 0
|
| 303 |
-
for file in files_to_download:
|
| 304 |
-
try:
|
| 305 |
-
size = ftp.size(file)
|
| 306 |
-
if size is not None:
|
| 307 |
-
total_size += size
|
| 308 |
-
except Exception as e:
|
| 309 |
-
self.controller.logger.warning(f"Could not get size for file {file}: {str(e)}")
|
| 310 |
-
|
| 311 |
-
downloaded_size = 0
|
| 312 |
-
|
| 313 |
-
# Download files
|
| 314 |
-
for file in files_to_download:
|
| 315 |
-
try:
|
| 316 |
-
self.status_updated.emit(f"Downloading: {file}")
|
| 317 |
-
file_type = 'FNA' if file.endswith('.fna.gz') else 'GBFF'
|
| 318 |
-
output_dir = os.path.join(self.controller.settings.CSPR_DB, file_type)
|
| 319 |
-
os.makedirs(output_dir, exist_ok=True)
|
| 320 |
-
|
| 321 |
-
local_filename = os.path.join(output_dir, file)
|
| 322 |
-
self.controller.logger.info(f"Downloading file: {file} to {local_filename}")
|
| 323 |
-
|
| 324 |
-
with open(local_filename, 'wb') as local_file:
|
| 325 |
-
def callback(data):
|
| 326 |
-
local_file.write(data)
|
| 327 |
-
nonlocal downloaded_size
|
| 328 |
-
downloaded_size += len(data)
|
| 329 |
-
if total_size > 0:
|
| 330 |
-
self.progress_updated.emit(self.id, downloaded_size, total_size)
|
| 331 |
-
|
| 332 |
-
ftp.retrbinary(f"RETR {file}", callback)
|
| 333 |
-
|
| 334 |
-
self.controller.logger.info(f"Download complete: {file}")
|
| 335 |
-
self.status_updated.emit(f"Decompressing: {file}")
|
| 336 |
-
|
| 337 |
-
self.controller.model.decompress_file(local_filename)
|
| 338 |
-
decompressed_filename = local_filename[:-3]
|
| 339 |
-
self.controller.model.add_downloaded_file(decompressed_filename)
|
| 340 |
-
|
| 341 |
-
except Exception as e:
|
| 342 |
-
self.controller.logger.error(f"Error downloading file {file}: {str(e)}")
|
| 343 |
-
continue
|
| 344 |
-
|
| 345 |
-
ftp.quit()
|
| 346 |
-
self.controller.logger.info(f"All files downloaded and decompressed successfully for ID: {self.id}")
|
| 347 |
-
self.all_completed.emit()
|
| 348 |
-
self.finished.emit(True)
|
| 349 |
-
|
| 350 |
-
except Exception as e:
|
| 351 |
-
self.controller.logger.error(f"Download error for ID {self.id}: {str(e)}", exc_info=True)
|
| 352 |
-
self.finished.emit(False)
|
|
|
|
| 1 |
import pandas as pd
|
| 2 |
from PyQt6 import QtCore, QtGui
|
| 3 |
from Bio import Entrez
|
|
|
|
| 4 |
from ftplib import FTP
|
| 5 |
+
from PyQt6.QtCore import Qt
|
| 6 |
import gzip
|
| 7 |
import os
|
|
|
|
| 8 |
import platform
|
| 9 |
+
import requests
|
|
|
|
|
|
|
|
|
|
| 10 |
from urllib.parse import urlparse
|
| 11 |
|
| 12 |
class NCBIWindowModel:
|
| 13 |
+
class DownloadThread(QtCore.QThread):
|
| 14 |
+
finished = QtCore.pyqtSignal(bool)
|
| 15 |
+
progress_updated = QtCore.pyqtSignal(int, int, int)
|
| 16 |
+
status_updated = QtCore.pyqtSignal(str)
|
| 17 |
+
all_completed = QtCore.pyqtSignal()
|
| 18 |
+
|
| 19 |
+
def __init__(self, controller, url, id, species_name, strain, download_fna, download_gbff):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.controller = controller
|
| 22 |
+
self.url = url
|
| 23 |
+
self.id = id
|
| 24 |
+
self.species_name = species_name
|
| 25 |
+
self.strain = strain
|
| 26 |
+
self.download_fna = download_fna
|
| 27 |
+
self.download_gbff = download_gbff
|
| 28 |
+
self.logger = controller.settings.get_logger()
|
| 29 |
+
self.db_path = controller.settings.get_db_path()
|
| 30 |
+
|
| 31 |
+
def run(self):
|
| 32 |
+
try:
|
| 33 |
+
parsed_url = urlparse(self.url)
|
| 34 |
+
if parsed_url.scheme == 'ftp':
|
| 35 |
+
self._download_ftp()
|
| 36 |
+
else:
|
| 37 |
+
self._download_http()
|
| 38 |
+
self.all_completed.emit()
|
| 39 |
+
self.finished.emit(True)
|
| 40 |
+
except Exception as e:
|
| 41 |
+
self.logger.error(f"Error in download thread: {str(e)}")
|
| 42 |
+
self.finished.emit(False)
|
| 43 |
+
|
| 44 |
+
def _download_http(self):
|
| 45 |
+
try:
|
| 46 |
+
self.status_updated.emit(f"Downloading {self.species_name} ({self.strain})")
|
| 47 |
+
response = requests.get(self.url, stream=True)
|
| 48 |
+
response.raise_for_status()
|
| 49 |
+
|
| 50 |
+
total_size = int(response.headers.get('content-length', 0))
|
| 51 |
+
is_gzipped = self.url.endswith('.gz') or 'gzip=true' in self.url
|
| 52 |
+
is_fna = '.fna.' in self.url.lower() or 'fasta' in self.url.lower()
|
| 53 |
+
|
| 54 |
+
file_type = 'FNA' if is_fna else 'GBFF'
|
| 55 |
+
extension = '.gz' if is_gzipped else ''
|
| 56 |
+
|
| 57 |
+
# Create output directory using the current database path
|
| 58 |
+
output_dir = os.path.join(self.db_path, file_type)
|
| 59 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 60 |
+
|
| 61 |
+
# Determine correct file extension and name
|
| 62 |
+
if is_fna:
|
| 63 |
+
local_filename = os.path.join(output_dir, f"{self.id}.fna{extension}")
|
| 64 |
+
else:
|
| 65 |
+
local_filename = os.path.join(output_dir, f"{self.id}.gbff{extension}")
|
| 66 |
+
|
| 67 |
+
downloaded_size = 0
|
| 68 |
+
with open(local_filename, 'wb') as f:
|
| 69 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 70 |
+
if chunk:
|
| 71 |
+
f.write(chunk)
|
| 72 |
+
downloaded_size += len(chunk)
|
| 73 |
+
if total_size:
|
| 74 |
+
progress = (downloaded_size / total_size) * 100
|
| 75 |
+
self.progress_updated.emit(self.id, progress, 100)
|
| 76 |
+
|
| 77 |
+
if not os.path.exists(local_filename) or os.path.getsize(local_filename) == 0:
|
| 78 |
+
raise Exception(f"Downloaded file {local_filename} is empty or does not exist")
|
| 79 |
+
|
| 80 |
+
if is_gzipped:
|
| 81 |
+
try:
|
| 82 |
+
# Read the gzipped content and write to uncompressed file
|
| 83 |
+
uncompressed_filename = local_filename[:-3] # Remove .gz
|
| 84 |
+
with gzip.open(local_filename, 'rb') as f_in:
|
| 85 |
+
content = f_in.read()
|
| 86 |
+
if not content:
|
| 87 |
+
raise Exception("Decompressed content is empty")
|
| 88 |
+
with open(uncompressed_filename, 'wb') as f_out:
|
| 89 |
+
f_out.write(content)
|
| 90 |
+
|
| 91 |
+
# Verify uncompressed file
|
| 92 |
+
if not os.path.exists(uncompressed_filename) or os.path.getsize(uncompressed_filename) == 0:
|
| 93 |
+
raise Exception(f"Decompressed file is empty")
|
| 94 |
+
|
| 95 |
+
# Remove the gzipped file only if decompression was successful
|
| 96 |
+
os.remove(local_filename)
|
| 97 |
+
self.controller.model.add_downloaded_file(uncompressed_filename)
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
self.logger.error(f"Error decompressing file: {str(e)}")
|
| 101 |
+
# Try to handle the file as non-gzipped if decompression fails
|
| 102 |
+
if os.path.exists(local_filename) and os.path.getsize(local_filename) > 0:
|
| 103 |
+
uncompressed_filename = local_filename[:-3]
|
| 104 |
+
os.rename(local_filename, uncompressed_filename)
|
| 105 |
+
self.controller.model.add_downloaded_file(uncompressed_filename)
|
| 106 |
+
else:
|
| 107 |
+
raise
|
| 108 |
+
else:
|
| 109 |
+
self.controller.model.add_downloaded_file(local_filename)
|
| 110 |
+
|
| 111 |
+
self.status_updated.emit(f"Download complete: {self.species_name}")
|
| 112 |
+
|
| 113 |
+
except Exception as e:
|
| 114 |
+
self.logger.error(f"Error in HTTP download: {str(e)}")
|
| 115 |
+
raise
|
| 116 |
+
|
| 117 |
+
def _download_ftp(self):
|
| 118 |
+
try:
|
| 119 |
+
self.status_updated.emit(f"Downloading {self.species_name} ({self.strain})")
|
| 120 |
+
|
| 121 |
+
parsed_url = urlparse(self.url)
|
| 122 |
+
ftp = FTP(parsed_url.netloc)
|
| 123 |
+
ftp.login()
|
| 124 |
+
|
| 125 |
+
file_size = ftp.size(parsed_url.path[1:])
|
| 126 |
+
is_gzipped = self.url.endswith('.gz')
|
| 127 |
+
is_fna = '.fna.' in self.url.lower() or '.fasta.' in self.url.lower()
|
| 128 |
+
|
| 129 |
+
file_type = 'FNA' if is_fna else 'GBFF'
|
| 130 |
+
extension = '.gz' if is_gzipped else ''
|
| 131 |
+
|
| 132 |
+
local_filename = os.path.join(
|
| 133 |
+
self.db_path,
|
| 134 |
+
file_type,
|
| 135 |
+
f"{self.id}.{file_type.lower()}{extension}"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
os.makedirs(os.path.dirname(local_filename), exist_ok=True)
|
| 139 |
+
|
| 140 |
+
downloaded_size = 0
|
| 141 |
+
with open(local_filename, 'wb') as f:
|
| 142 |
+
def callback(data):
|
| 143 |
+
nonlocal downloaded_size
|
| 144 |
+
f.write(data)
|
| 145 |
+
downloaded_size += len(data)
|
| 146 |
+
if file_size:
|
| 147 |
+
progress = (downloaded_size / file_size) * 100
|
| 148 |
+
self.progress_updated.emit(self.id, progress, 100)
|
| 149 |
+
|
| 150 |
+
ftp.retrbinary(f'RETR {parsed_url.path[1:]}', callback)
|
| 151 |
+
|
| 152 |
+
ftp.quit()
|
| 153 |
+
|
| 154 |
+
if not os.path.exists(local_filename) or os.path.getsize(local_filename) == 0:
|
| 155 |
+
raise Exception(f"Downloaded file {local_filename} is empty or does not exist")
|
| 156 |
+
|
| 157 |
+
if is_gzipped:
|
| 158 |
+
self._decompress_file(local_filename)
|
| 159 |
+
else:
|
| 160 |
+
self.controller.model.add_downloaded_file(local_filename)
|
| 161 |
+
|
| 162 |
+
self.status_updated.emit(f"Download complete: {self.species_name}")
|
| 163 |
+
|
| 164 |
+
except Exception as e:
|
| 165 |
+
self.logger.error(f"Error in FTP download: {str(e)}")
|
| 166 |
+
raise
|
| 167 |
+
|
| 168 |
+
def _decompress_file(self, filename):
|
| 169 |
+
try:
|
| 170 |
+
with gzip.open(filename, 'rb') as f_in:
|
| 171 |
+
decompressed_filename = filename[:-3]
|
| 172 |
+
with open(decompressed_filename, 'wb') as f_out:
|
| 173 |
+
f_out.write(f_in.read())
|
| 174 |
+
|
| 175 |
+
if not os.path.exists(decompressed_filename) or os.path.getsize(decompressed_filename) == 0:
|
| 176 |
+
raise Exception(f"Decompressed file {decompressed_filename} is empty or does not exist")
|
| 177 |
+
|
| 178 |
+
os.remove(filename)
|
| 179 |
+
self.controller.model.add_downloaded_file(decompressed_filename)
|
| 180 |
+
|
| 181 |
+
except gzip.BadGzipFile:
|
| 182 |
+
self.logger.warning(f"File {filename} is not actually gzipped, renaming without .gz extension")
|
| 183 |
+
decompressed_filename = filename[:-3]
|
| 184 |
+
os.rename(filename, decompressed_filename)
|
| 185 |
+
self.controller.model.add_downloaded_file(decompressed_filename)
|
| 186 |
+
|
| 187 |
def __init__(self, settings):
|
| 188 |
self.settings = settings
|
| 189 |
self.logger = settings.get_logger()
|
| 190 |
self.df = pd.DataFrame()
|
| 191 |
self.genbank_ftp_dict = {}
|
| 192 |
self.refseq_ftp_dict = {}
|
| 193 |
+
self.files = []
|
| 194 |
+
self.current_database = None
|
| 195 |
+
self.current_search_params = {}
|
| 196 |
+
self.download_fna = False
|
| 197 |
+
self.download_gbff = False
|
| 198 |
+
Entrez.email = "your_email@example.com"
|
| 199 |
|
| 200 |
+
self.database_apis = {
|
| 201 |
+
"NCBI GenBank": self._search_ncbi,
|
| 202 |
+
"ENA (European Nucleotide Archive)": self._search_ena,
|
| 203 |
+
"UCSC Genome Browser": self._search_ucsc
|
| 204 |
+
}
|
| 205 |
|
| 206 |
def search_ncbi(self, search_params):
|
| 207 |
+
"""Search selected database with given parameters"""
|
| 208 |
+
database = search_params.get('database', "NCBI GenBank")
|
| 209 |
+
search_function = self.database_apis.get(database)
|
| 210 |
+
|
| 211 |
+
if search_function:
|
| 212 |
+
# Store current database and search parameters
|
| 213 |
+
self.current_database = database
|
| 214 |
+
self.current_search_params = search_params
|
| 215 |
+
return search_function(search_params)
|
| 216 |
+
else:
|
| 217 |
+
self.logger.error(f"Unsupported database: {database}")
|
| 218 |
+
raise ValueError(f"Unsupported database: {database}")
|
| 219 |
+
|
| 220 |
+
def _search_ncbi(self, search_params):
|
| 221 |
+
"""Original NCBI search implementation"""
|
| 222 |
try:
|
| 223 |
retmax = int(search_params['max_results']) if search_params['max_results'] else 100
|
| 224 |
term = f'"{search_params["organism"]}"[Organism]'
|
| 225 |
|
| 226 |
if search_params['complete_genomes_only']:
|
| 227 |
term += ' AND "Complete Genome"[Assembly Level]'
|
| 228 |
+
if search_params['strain'] and search_params['strain'].strip():
|
| 229 |
term += f' AND "{search_params["strain"]}"[Infraspecific name]'
|
| 230 |
|
| 231 |
self.logger.info(f"Searching NCBI with term: {term}")
|
|
|
|
| 261 |
self.logger.error(f"Error in query_ncbi: {str(e)}", exc_info=True)
|
| 262 |
raise
|
| 263 |
|
| 264 |
+
def _search_ena(self, search_params):
|
| 265 |
+
"""Search ENA database"""
|
| 266 |
+
try:
|
| 267 |
+
organism = search_params['organism']
|
| 268 |
+
max_results = int(search_params['max_results'])
|
| 269 |
+
|
| 270 |
+
# ENA API endpoint for text search
|
| 271 |
+
base_url = "https://www.ebi.ac.uk/ena/portal/api/search"
|
| 272 |
+
|
| 273 |
+
# Construct query
|
| 274 |
+
query_parts = []
|
| 275 |
+
query_parts.append(f'scientific_name="{organism}"')
|
| 276 |
+
|
| 277 |
+
if search_params['strain']:
|
| 278 |
+
query_parts.append(f'strain="{search_params["strain"]}"')
|
| 279 |
+
|
| 280 |
+
if search_params['complete_genomes_only']:
|
| 281 |
+
query_parts.append('assembly_level="complete genome"')
|
| 282 |
+
|
| 283 |
+
query = " AND ".join(query_parts)
|
| 284 |
+
|
| 285 |
+
params = {
|
| 286 |
+
'result': 'assembly',
|
| 287 |
+
'query': query,
|
| 288 |
+
'limit': max_results,
|
| 289 |
+
'offset': 0,
|
| 290 |
+
'format': 'json',
|
| 291 |
+
'fields': 'accession,scientific_name,strain,assembly_level,assembly_name,study_accession'
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
self.logger.info(f"Searching ENA with parameters: {params}")
|
| 295 |
+
response = requests.get(base_url, params=params)
|
| 296 |
+
|
| 297 |
+
# Log the actual URL being called for debugging
|
| 298 |
+
self.logger.debug(f"ENA API URL: {response.url}")
|
| 299 |
+
|
| 300 |
+
# Check if the response is successful
|
| 301 |
+
if response.status_code != 200:
|
| 302 |
+
self.logger.error(f"ENA API error: {response.status_code} - {response.text}")
|
| 303 |
+
return pd.DataFrame()
|
| 304 |
+
|
| 305 |
+
# Parse JSON response
|
| 306 |
+
results = response.json()
|
| 307 |
+
|
| 308 |
+
if not results:
|
| 309 |
+
self.logger.info("No results returned from ENA")
|
| 310 |
+
return pd.DataFrame()
|
| 311 |
+
|
| 312 |
+
self.logger.info(f"Raw ENA response: {results[:2]}")
|
| 313 |
+
|
| 314 |
+
# Transform ENA results to match NCBI format
|
| 315 |
+
data = []
|
| 316 |
+
for result in results:
|
| 317 |
+
entry = {
|
| 318 |
+
'ID': result.get('accession', 'N/A'),
|
| 319 |
+
'Species Name': result.get('scientific_name', 'N/A'),
|
| 320 |
+
'Strain': result.get('strain', 'N/A'),
|
| 321 |
+
'Assembly Name': result.get('assembly_name', 'N/A'),
|
| 322 |
+
'RefSeq assembly accession': 'Not Available',
|
| 323 |
+
'GenBank assembly accession': result.get('accession', 'N/A'),
|
| 324 |
+
'Assembly Status': result.get('assembly_level', 'N/A')
|
| 325 |
+
}
|
| 326 |
+
data.append(entry)
|
| 327 |
+
|
| 328 |
+
self.df = pd.DataFrame(data)
|
| 329 |
+
self.logger.info(f"Found {len(self.df)} results from ENA")
|
| 330 |
+
return self.df
|
| 331 |
+
|
| 332 |
+
except Exception as e:
|
| 333 |
+
self.logger.error(f"Error searching ENA: {str(e)}")
|
| 334 |
+
raise
|
| 335 |
+
|
| 336 |
+
def _search_ucsc(self, search_params):
|
| 337 |
+
"""Search UCSC Genome Browser database"""
|
| 338 |
+
try:
|
| 339 |
+
organism = search_params['organism']
|
| 340 |
+
max_results = int(search_params['max_results'])
|
| 341 |
+
|
| 342 |
+
# Get list of genomes
|
| 343 |
+
base_url = "https://api.genome.ucsc.edu"
|
| 344 |
+
response = requests.get(f"{base_url}/list/ucscGenomes")
|
| 345 |
+
response.raise_for_status()
|
| 346 |
+
|
| 347 |
+
# Log raw response for debugging
|
| 348 |
+
self.logger.debug(f"UCSC API response: {response.text[:500]}")
|
| 349 |
+
|
| 350 |
+
data = response.json()
|
| 351 |
+
if 'ucscGenomes' not in data:
|
| 352 |
+
self.logger.warning("No ucscGenomes field in UCSC response")
|
| 353 |
+
return pd.DataFrame()
|
| 354 |
+
|
| 355 |
+
# Filter genomes by organism name
|
| 356 |
+
matching_genomes = []
|
| 357 |
+
for genome_id, genome in data['ucscGenomes'].items():
|
| 358 |
+
# Search in both organism and scientificName fields
|
| 359 |
+
if (organism.lower() in genome.get('organism', '').lower() or
|
| 360 |
+
organism.lower() in genome.get('scientificName', '').lower()):
|
| 361 |
+
|
| 362 |
+
# Get detailed track info
|
| 363 |
+
track_response = requests.get(f"{base_url}/list/tracks", params={'genome': genome_id})
|
| 364 |
+
if track_response.status_code == 200:
|
| 365 |
+
tracks = track_response.json()
|
| 366 |
+
genome['tracks'] = tracks
|
| 367 |
+
matching_genomes.append(genome)
|
| 368 |
+
|
| 369 |
+
if len(matching_genomes) >= max_results:
|
| 370 |
+
break
|
| 371 |
+
|
| 372 |
+
if not matching_genomes:
|
| 373 |
+
self.logger.info("No results returned from UCSC")
|
| 374 |
+
return pd.DataFrame()
|
| 375 |
+
|
| 376 |
+
# Transform UCSC results to match NCBI format
|
| 377 |
+
data = []
|
| 378 |
+
for genome in matching_genomes:
|
| 379 |
+
entry = {
|
| 380 |
+
'ID': genome.get('genome', 'N/A'),
|
| 381 |
+
'Species Name': genome.get('scientificName', genome.get('organism', 'N/A')),
|
| 382 |
+
'Strain': genome.get('description', 'N/A'),
|
| 383 |
+
'Assembly Name': genome.get('sourceName', 'N/A'),
|
| 384 |
+
'RefSeq assembly accession': 'Not Available', # UCSC doesn't provide RefSeq accessions
|
| 385 |
+
'GenBank assembly accession': genome.get('sourceName', 'Not Available'),
|
| 386 |
+
'Assembly Status': 'Complete' if genome.get('active') else 'N/A'
|
| 387 |
+
}
|
| 388 |
+
data.append(entry)
|
| 389 |
+
|
| 390 |
+
self.df = pd.DataFrame(data)
|
| 391 |
+
self.logger.info(f"Found {len(self.df)} results from UCSC")
|
| 392 |
+
return self.df
|
| 393 |
+
|
| 394 |
+
except Exception as e:
|
| 395 |
+
self.logger.error(f"Error searching UCSC: {str(e)}")
|
| 396 |
+
raise
|
| 397 |
+
|
| 398 |
def _process_ncbi_data(self, summary_results):
|
| 399 |
data = []
|
| 400 |
for assembly in summary_results['DocumentSummarySet']['DocumentSummary']:
|
| 401 |
gb_ftp = assembly.get('FtpPath_GenBank', '')
|
| 402 |
rs_ftp = assembly.get('FtpPath_RefSeq', '')
|
| 403 |
|
| 404 |
+
# Get assembly accession and uid
|
| 405 |
+
assembly_accession = assembly.get('AssemblyAccession', 'N/A')
|
| 406 |
+
uid = assembly.attributes['uid']
|
| 407 |
+
|
| 408 |
+
# Safely get the strain information
|
| 409 |
+
biosource = assembly.get('Biosource', {})
|
| 410 |
+
infraspecies_list = biosource.get('InfraspeciesList', [])
|
| 411 |
+
strain = (infraspecies_list[0].get('Sub_value', 'N/A')
|
| 412 |
+
if infraspecies_list
|
| 413 |
+
else 'N/A')
|
| 414 |
+
|
| 415 |
entry = {
|
| 416 |
+
'ID': uid,
|
| 417 |
'Species Name': assembly.get('SpeciesName', 'N/A'),
|
| 418 |
+
'Strain': strain,
|
| 419 |
'Assembly Name': assembly.get('AssemblyName', 'N/A'),
|
| 420 |
+
'RefSeq assembly accession': assembly_accession if rs_ftp else 'Not Available',
|
| 421 |
+
'GenBank assembly accession': assembly_accession if gb_ftp else 'Not Available',
|
| 422 |
'Assembly Status': assembly.get('AssemblyStatus', 'N/A')
|
| 423 |
}
|
| 424 |
|
| 425 |
+
# Store FTP paths using both UID and assembly accession as keys
|
| 426 |
if gb_ftp:
|
| 427 |
+
self.genbank_ftp_dict[uid] = gb_ftp
|
| 428 |
+
self.genbank_ftp_dict[assembly_accession] = gb_ftp
|
| 429 |
+
|
| 430 |
if rs_ftp:
|
| 431 |
+
self.refseq_ftp_dict[uid] = rs_ftp
|
| 432 |
+
self.refseq_ftp_dict[assembly_accession] = rs_ftp
|
| 433 |
+
|
| 434 |
data.append(entry)
|
| 435 |
|
| 436 |
self.df = pd.DataFrame(data)
|
|
|
|
| 440 |
self.logger.warning("No data found in NCBI response")
|
| 441 |
else:
|
| 442 |
self.logger.info(f"Processed {len(self.df)} entries from NCBI response")
|
| 443 |
+
self.logger.debug(f"GenBank FTP dict has {len(self.genbank_ftp_dict)} entries")
|
| 444 |
+
self.logger.debug(f"RefSeq FTP dict has {len(self.refseq_ftp_dict)} entries")
|
|
|
|
|
|
|
| 445 |
|
| 446 |
def _get_element_text(self, element, tag):
|
| 447 |
el = element.find(tag)
|
| 448 |
return el.text if el is not None else 'N/A'
|
| 449 |
|
| 450 |
def get_download_url(self, id, use_genbank):
|
| 451 |
+
"""Get download URL based on selected database and parameters"""
|
| 452 |
+
try:
|
| 453 |
+
database = self.current_database
|
| 454 |
+
|
| 455 |
+
# Set download flags based on search parameters
|
| 456 |
+
self.download_fna = self.current_search_params.get('fna', False)
|
| 457 |
+
self.download_gbff = self.current_search_params.get('gbff', False)
|
| 458 |
+
|
| 459 |
+
if database == "NCBI GenBank":
|
| 460 |
+
return self._get_ncbi_download_url(id, use_genbank)
|
| 461 |
+
elif database == "ENA (European Nucleotide Archive)":
|
| 462 |
+
return self._get_ena_download_url(id)
|
| 463 |
+
elif database == "UCSC Genome Browser":
|
| 464 |
+
self.logger.warning("Downloads not supported for UCSC Genome Browser")
|
| 465 |
+
return None
|
| 466 |
+
|
| 467 |
+
return None
|
| 468 |
+
except Exception as e:
|
| 469 |
+
self.logger.error(f"Error getting download URL: {str(e)}")
|
| 470 |
+
return None
|
| 471 |
+
|
| 472 |
+
def _get_ena_download_url(self, id):
|
| 473 |
+
"""Get download URL for ENA database"""
|
| 474 |
+
try:
|
| 475 |
+
urls = []
|
| 476 |
+
|
| 477 |
+
# Try to get FASTA (FNA) file
|
| 478 |
+
fasta_url = f"https://www.ebi.ac.uk/ena/browser/api/fasta/{id}?download=true&gzip=true"
|
| 479 |
+
test_response = requests.head(fasta_url)
|
| 480 |
+
if test_response.status_code == 200:
|
| 481 |
+
self.logger.info(f"Found direct FASTA download URL for {id}")
|
| 482 |
+
urls.append(fasta_url)
|
| 483 |
+
|
| 484 |
+
# Try to get EMBL (GBFF) file
|
| 485 |
+
embl_url = f"https://www.ebi.ac.uk/ena/browser/api/embl/{id}?download=true&gzip=true"
|
| 486 |
+
test_response = requests.head(embl_url)
|
| 487 |
+
if test_response.status_code == 200:
|
| 488 |
+
self.logger.info(f"Found direct EMBL download URL for {id}")
|
| 489 |
+
urls.append(embl_url)
|
| 490 |
+
|
| 491 |
+
# If no direct downloads found, try alternative sources
|
| 492 |
+
if not urls:
|
| 493 |
+
# Try WGS format if it's a WGS accession
|
| 494 |
+
if len(id) >= 6 and id.startswith('GCA_'):
|
| 495 |
+
wgs_id = id.split('_')[1] # Get the numeric part
|
| 496 |
+
wgs_fasta_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/wgs/{wgs_id[:6].lower()}/{wgs_id}.fasta.gz"
|
| 497 |
+
wgs_embl_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/wgs/{wgs_id[:6].lower()}/{wgs_id}.embl.gz"
|
| 498 |
+
|
| 499 |
+
test_response = requests.head(wgs_fasta_url)
|
| 500 |
+
if test_response.status_code == 200:
|
| 501 |
+
self.logger.info(f"Found WGS FASTA download URL for {id}")
|
| 502 |
+
urls.append(wgs_fasta_url)
|
| 503 |
+
|
| 504 |
+
test_response = requests.head(wgs_embl_url)
|
| 505 |
+
if test_response.status_code == 200:
|
| 506 |
+
self.logger.info(f"Found WGS EMBL download URL for {id}")
|
| 507 |
+
urls.append(wgs_embl_url)
|
| 508 |
+
|
| 509 |
+
# Try assembly FTP path
|
| 510 |
+
assembly_fasta_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/assembly/{id}/{id}.fasta.gz"
|
| 511 |
+
assembly_embl_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/assembly/{id}/{id}.embl.gz"
|
| 512 |
+
|
| 513 |
+
test_response = requests.head(assembly_fasta_url)
|
| 514 |
+
if test_response.status_code == 200:
|
| 515 |
+
self.logger.info(f"Found assembly FASTA URL for {id}")
|
| 516 |
+
urls.append(assembly_fasta_url)
|
| 517 |
+
|
| 518 |
+
test_response = requests.head(assembly_embl_url)
|
| 519 |
+
if test_response.status_code == 200:
|
| 520 |
+
self.logger.info(f"Found assembly EMBL URL for {id}")
|
| 521 |
+
urls.append(assembly_embl_url)
|
| 522 |
+
|
| 523 |
+
if urls:
|
| 524 |
+
self.logger.info(f"Found {len(urls)} download URLs for {id}: {urls}")
|
| 525 |
+
return urls
|
| 526 |
+
|
| 527 |
+
self.logger.warning(f"No suitable download URLs found for accession {id}")
|
| 528 |
+
return None
|
| 529 |
+
|
| 530 |
+
except Exception as e:
|
| 531 |
+
self.logger.error(f"Error getting ENA download URLs for {id}: {str(e)}")
|
| 532 |
+
return None
|
| 533 |
+
|
| 534 |
+
def _get_ncbi_download_url(self, id, use_genbank):
|
| 535 |
+
try:
|
| 536 |
+
# First try with the ID directly
|
| 537 |
+
base_url = None
|
| 538 |
+
if use_genbank:
|
| 539 |
+
base_url = self.genbank_ftp_dict.get(id)
|
| 540 |
+
else:
|
| 541 |
+
base_url = self.refseq_ftp_dict.get(id)
|
| 542 |
+
|
| 543 |
+
# If not found, try to find the corresponding assembly accession in our DataFrame
|
| 544 |
+
if not base_url and id in self.df.index:
|
| 545 |
+
row = self.df.loc[id]
|
| 546 |
+
accession = row['GenBank assembly accession'] if use_genbank else row['RefSeq assembly accession']
|
| 547 |
+
if accession != 'Not Available':
|
| 548 |
+
if use_genbank:
|
| 549 |
+
base_url = self.genbank_ftp_dict.get(accession)
|
| 550 |
+
else:
|
| 551 |
+
base_url = self.refseq_ftp_dict.get(accession)
|
| 552 |
+
|
| 553 |
+
if not base_url:
|
| 554 |
+
self.logger.warning(f"No FTP path found for ID {id}")
|
| 555 |
+
return None
|
| 556 |
+
|
| 557 |
+
# Remove trailing slash if present
|
| 558 |
+
base_url = base_url.rstrip('/')
|
| 559 |
+
|
| 560 |
+
# Get the assembly accession from the base URL
|
| 561 |
+
assembly_dir = os.path.basename(base_url)
|
| 562 |
+
|
| 563 |
+
# Return both FNA and GBFF URLs if they exist
|
| 564 |
+
urls = []
|
| 565 |
+
|
| 566 |
+
# Check FNA file
|
| 567 |
+
fna_url = f"{base_url}/{assembly_dir}_genomic.fna.gz"
|
| 568 |
+
gbff_url = f"{base_url}/{assembly_dir}_genomic.gbff.gz"
|
| 569 |
+
|
| 570 |
+
# Test if URLs exist
|
| 571 |
+
try:
|
| 572 |
+
parsed_url = urlparse(fna_url)
|
| 573 |
+
ftp = FTP(parsed_url.netloc)
|
| 574 |
+
ftp.login()
|
| 575 |
+
|
| 576 |
+
# Check FNA file
|
| 577 |
+
try:
|
| 578 |
+
ftp.size(parsed_url.path[1:])
|
| 579 |
+
urls.append(fna_url)
|
| 580 |
+
except Exception:
|
| 581 |
+
# Try alternative FNA name format
|
| 582 |
+
alt_fna_url = f"{base_url}/{assembly_dir}.fna.gz"
|
| 583 |
+
try:
|
| 584 |
+
ftp.size(urlparse(alt_fna_url).path[1:])
|
| 585 |
+
urls.append(alt_fna_url)
|
| 586 |
+
except:
|
| 587 |
+
pass
|
| 588 |
+
|
| 589 |
+
# Check GBFF file
|
| 590 |
+
try:
|
| 591 |
+
ftp.size(urlparse(gbff_url).path[1:])
|
| 592 |
+
urls.append(gbff_url)
|
| 593 |
+
except Exception:
|
| 594 |
+
# Try alternative GBFF name format
|
| 595 |
+
alt_gbff_url = f"{base_url}/{assembly_dir}.gbff.gz"
|
| 596 |
+
try:
|
| 597 |
+
ftp.size(urlparse(alt_gbff_url).path[1:])
|
| 598 |
+
urls.append(alt_gbff_url)
|
| 599 |
+
except:
|
| 600 |
+
pass
|
| 601 |
+
|
| 602 |
+
ftp.quit()
|
| 603 |
+
|
| 604 |
+
return urls if urls else None
|
| 605 |
+
|
| 606 |
+
except Exception as e:
|
| 607 |
+
self.logger.error(f"Error checking FTP URLs for {id}: {str(e)}")
|
| 608 |
+
return None
|
| 609 |
+
|
| 610 |
+
except Exception as e:
|
| 611 |
+
self.logger.error(f"Error getting NCBI download URL for {id}: {str(e)}")
|
| 612 |
+
return None
|
| 613 |
|
| 614 |
def decompress_file(self, filename):
|
| 615 |
try:
|
|
|
|
| 628 |
raise
|
| 629 |
|
| 630 |
def get_output_path(self, file_type):
|
| 631 |
+
db_path = self.settings.get_db_path()
|
| 632 |
+
self.logger.debug(f"Using database path for downloads: {db_path}")
|
| 633 |
+
return os.path.join(db_path, file_type)
|
|
|
|
| 634 |
|
| 635 |
def rename_file(self, old_name, new_name, file_type):
|
| 636 |
old_path = os.path.join(self.get_output_path(file_type), old_name)
|
|
|
|
| 728 |
if regex.match(text).hasMatch():
|
| 729 |
return False
|
| 730 |
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -37,19 +37,30 @@ class NewEndonucleaseModel(QObject):
|
|
| 37 |
return [], [] # Return empty lists if there's an error
|
| 38 |
|
| 39 |
def create_new_endonuclease(self, new_endonuclease_str):
|
|
|
|
| 40 |
try:
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
| 43 |
for line in f:
|
| 44 |
-
|
| 45 |
-
if 'ENDONUCLEASES'
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
self.global_settings.config_manager.load_endonucleases_data()
|
| 50 |
self.endonuclease_updated.emit()
|
|
|
|
| 51 |
except Exception as e:
|
| 52 |
-
show_error(self.global_settings, "Error in create_new_endonuclease()
|
| 53 |
|
| 54 |
def is_duplicate_abbreviation(self, abbr):
|
| 55 |
try:
|
|
@@ -66,76 +77,90 @@ class NewEndonucleaseModel(QObject):
|
|
| 66 |
return False
|
| 67 |
|
| 68 |
def create_endonuclease_string(self, form_data):
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
pam = ','.join([x.strip() for x in pam.split(',')])
|
| 72 |
-
|
| 73 |
argument_list = [
|
| 74 |
form_data['endonuclease_abbreviation'],
|
| 75 |
-
|
| 76 |
form_data['endonuclease_five_prime_length'],
|
| 77 |
form_data['endonuclease_seed_length'],
|
| 78 |
form_data['endonuclease_three_prime_length'],
|
| 79 |
-
form_data['endonuclease_direction'],
|
| 80 |
form_data['endonuclease_organism'],
|
| 81 |
form_data['endonuclease_CRISPR_type'],
|
| 82 |
form_data['endonuclease_on_target_scoring'],
|
| 83 |
form_data['endonuclease_off_target_scoring']
|
| 84 |
]
|
| 85 |
-
|
| 86 |
return ";".join(str(arg) for arg in argument_list)
|
| 87 |
|
| 88 |
def update_endonuclease(self, selected, form_data):
|
|
|
|
| 89 |
try:
|
| 90 |
new_endonuclease_str = self.create_endonuclease_string(form_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
with open(self.casper_info_path, 'r') as f:
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
updated = True
|
| 103 |
-
break
|
| 104 |
|
| 105 |
-
if updated:
|
| 106 |
-
with open(self.casper_info_path, 'w') as f:
|
| 107 |
-
f.writelines(lines)
|
| 108 |
-
self.global_settings.config_manager.load_endonucleases_data()
|
| 109 |
-
self.endonuclease_updated.emit()
|
| 110 |
-
else:
|
| 111 |
-
raise ValueError(f"Endonuclease '{selected}' not found in CASPERinfo file")
|
| 112 |
except Exception as e:
|
| 113 |
show_error(self.global_settings, "Error updating endonuclease", str(e))
|
| 114 |
|
| 115 |
def delete_endonuclease(self, selected):
|
|
|
|
| 116 |
try:
|
| 117 |
-
with open(self.casper_info_path, 'r') as f:
|
| 118 |
-
lines = f.readlines()
|
| 119 |
-
|
| 120 |
-
deleted = False
|
| 121 |
new_lines = []
|
|
|
|
| 122 |
selected_abbr = selected.split(' - ')[0]
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
new_lines.append(line)
|
| 126 |
-
continue
|
| 127 |
-
fields = line.strip().split(';')
|
| 128 |
-
if len(fields) >= 2 and fields[1] == selected_abbr:
|
| 129 |
-
deleted = True
|
| 130 |
-
continue # Skip this line to delete it
|
| 131 |
-
new_lines.append(line) # Keep all other lines
|
| 132 |
|
| 133 |
-
if deleted:
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
| 140 |
except Exception as e:
|
| 141 |
show_error(self.global_settings, "Error deleting endonuclease", str(e))
|
|
|
|
| 37 |
return [], [] # Return empty lists if there's an error
|
| 38 |
|
| 39 |
def create_new_endonuclease(self, new_endonuclease_str):
|
| 40 |
+
"""Add a new endonuclease to CASPERinfo file"""
|
| 41 |
try:
|
| 42 |
+
found_section = False
|
| 43 |
+
new_lines = []
|
| 44 |
+
|
| 45 |
+
with open(self.casper_info_path, 'r') as f:
|
| 46 |
for line in f:
|
| 47 |
+
new_lines.append(line)
|
| 48 |
+
if line.strip() == 'ENDONUCLEASES':
|
| 49 |
+
found_section = True
|
| 50 |
+
new_lines.append(new_endonuclease_str + '\n')
|
| 51 |
+
|
| 52 |
+
if not found_section:
|
| 53 |
+
new_lines.append('ENDONUCLEASES\n')
|
| 54 |
+
new_lines.append(new_endonuclease_str + '\n')
|
| 55 |
+
|
| 56 |
+
with open(self.casper_info_path, 'w') as f:
|
| 57 |
+
f.writelines(new_lines)
|
| 58 |
+
|
| 59 |
self.global_settings.config_manager.load_endonucleases_data()
|
| 60 |
self.endonuclease_updated.emit()
|
| 61 |
+
|
| 62 |
except Exception as e:
|
| 63 |
+
show_error(self.global_settings, "Error in create_new_endonuclease()", str(e))
|
| 64 |
|
| 65 |
def is_duplicate_abbreviation(self, abbr):
|
| 66 |
try:
|
|
|
|
| 77 |
return False
|
| 78 |
|
| 79 |
def create_endonuclease_string(self, form_data):
|
| 80 |
+
"""Create a properly formatted endonuclease string for CASPERinfo"""
|
| 81 |
+
# Format: abbr;PAM;5_prime_length;seed_length;3_prime_length;direction;organism;CRISPR_type;on_target;off_target
|
|
|
|
|
|
|
| 82 |
argument_list = [
|
| 83 |
form_data['endonuclease_abbreviation'],
|
| 84 |
+
form_data['endonuclease_pam_sequence'],
|
| 85 |
form_data['endonuclease_five_prime_length'],
|
| 86 |
form_data['endonuclease_seed_length'],
|
| 87 |
form_data['endonuclease_three_prime_length'],
|
| 88 |
+
'3' if form_data['endonuclease_direction'] == '3' else '5',
|
| 89 |
form_data['endonuclease_organism'],
|
| 90 |
form_data['endonuclease_CRISPR_type'],
|
| 91 |
form_data['endonuclease_on_target_scoring'],
|
| 92 |
form_data['endonuclease_off_target_scoring']
|
| 93 |
]
|
|
|
|
| 94 |
return ";".join(str(arg) for arg in argument_list)
|
| 95 |
|
| 96 |
def update_endonuclease(self, selected, form_data):
|
| 97 |
+
"""Update an existing endonuclease in CASPERinfo file"""
|
| 98 |
try:
|
| 99 |
new_endonuclease_str = self.create_endonuclease_string(form_data)
|
| 100 |
+
new_lines = []
|
| 101 |
+
updated = False
|
| 102 |
+
selected_abbr = selected.split(' - ')[0] # Get abbreviation part
|
| 103 |
+
|
| 104 |
with open(self.casper_info_path, 'r') as f:
|
| 105 |
+
in_endo_section = False
|
| 106 |
+
for line in f:
|
| 107 |
+
if line.strip() == 'ENDONUCLEASES':
|
| 108 |
+
in_endo_section = True
|
| 109 |
+
new_lines.append(line)
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
if in_endo_section and line.strip():
|
| 113 |
+
fields = line.strip().split(';')
|
| 114 |
+
if fields[0] == selected_abbr:
|
| 115 |
+
new_lines.append(new_endonuclease_str + '\n')
|
| 116 |
+
updated = True
|
| 117 |
+
else:
|
| 118 |
+
new_lines.append(line)
|
| 119 |
+
else:
|
| 120 |
+
new_lines.append(line)
|
| 121 |
|
| 122 |
+
if not updated:
|
| 123 |
+
raise ValueError(f"Endonuclease '{selected}' not found")
|
| 124 |
+
|
| 125 |
+
with open(self.casper_info_path, 'w') as f:
|
| 126 |
+
f.writelines(new_lines)
|
| 127 |
+
|
| 128 |
+
self.global_settings.config_manager.load_endonucleases_data()
|
| 129 |
+
self.endonuclease_updated.emit()
|
|
|
|
|
|
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
except Exception as e:
|
| 132 |
show_error(self.global_settings, "Error updating endonuclease", str(e))
|
| 133 |
|
| 134 |
def delete_endonuclease(self, selected):
|
| 135 |
+
"""Delete an endonuclease from CASPERinfo file"""
|
| 136 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
new_lines = []
|
| 138 |
+
deleted = False
|
| 139 |
selected_abbr = selected.split(' - ')[0]
|
| 140 |
+
|
| 141 |
+
with open(self.casper_info_path, 'r') as f:
|
| 142 |
+
in_endo_section = False
|
| 143 |
+
for line in f:
|
| 144 |
+
if line.strip() == 'ENDONUCLEASES':
|
| 145 |
+
in_endo_section = True
|
| 146 |
+
new_lines.append(line)
|
| 147 |
+
continue
|
| 148 |
+
|
| 149 |
+
if in_endo_section and line.strip():
|
| 150 |
+
fields = line.strip().split(';')
|
| 151 |
+
if fields[0] == selected_abbr:
|
| 152 |
+
deleted = True
|
| 153 |
+
continue
|
| 154 |
new_lines.append(line)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
+
if not deleted:
|
| 157 |
+
raise ValueError(f"Endonuclease '{selected}' not found")
|
| 158 |
+
|
| 159 |
+
with open(self.casper_info_path, 'w') as f:
|
| 160 |
+
f.writelines(new_lines)
|
| 161 |
+
|
| 162 |
+
self.global_settings.config_manager.load_endonucleases_data()
|
| 163 |
+
self.endonuclease_updated.emit()
|
| 164 |
+
|
| 165 |
except Exception as e:
|
| 166 |
show_error(self.global_settings, "Error deleting endonuclease", str(e))
|
|
@@ -66,12 +66,11 @@ class NewGenomeWindowModel(QObject):
|
|
| 66 |
def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
|
| 67 |
db_path = self.settings.get_db_path()
|
| 68 |
|
| 69 |
-
#
|
| 70 |
-
if db_path.endswith(
|
| 71 |
-
db_path = db_path
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
db_path = db_path.rstrip('/') + '/'
|
| 75 |
|
| 76 |
print(f"The endonuclease data is {endonuclease_data}")
|
| 77 |
|
|
@@ -85,13 +84,17 @@ class NewGenomeWindowModel(QObject):
|
|
| 85 |
endonuclease_data['endonuclease_seed_length'],
|
| 86 |
endonuclease_data['endonuclease_three_prime_length'],
|
| 87 |
organism_code,
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
f'{organism_name} {strain}',
|
| 92 |
'notes',
|
| 93 |
f'DATA:{endonuclease_data["endonuclease_on_target_scoring"]}'
|
| 94 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
return arguments
|
| 96 |
|
| 97 |
def get_arguments_command_for_job(self, job_index):
|
|
@@ -103,7 +106,7 @@ class NewGenomeWindowModel(QObject):
|
|
| 103 |
if platform.system() == 'Windows':
|
| 104 |
program = f'"{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Win.exe")}" '
|
| 105 |
elif platform.system() == 'Linux':
|
| 106 |
-
program = f'
|
| 107 |
else:
|
| 108 |
program = f'{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Mac")}'
|
| 109 |
return program
|
|
@@ -145,3 +148,10 @@ class NewGenomeWindowModel(QObject):
|
|
| 145 |
if not endonuclease:
|
| 146 |
return None
|
| 147 |
return self.endonucleases.get(endonuclease, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
|
| 67 |
db_path = self.settings.get_db_path()
|
| 68 |
|
| 69 |
+
# Ensure db_path ends with a forward slash
|
| 70 |
+
if not db_path.endswith('/'):
|
| 71 |
+
db_path = f"{db_path}/"
|
| 72 |
+
|
| 73 |
+
self.logger.debug(f"Using database path: {db_path}") # Add logging
|
|
|
|
| 74 |
|
| 75 |
print(f"The endonuclease data is {endonuclease_data}")
|
| 76 |
|
|
|
|
| 84 |
endonuclease_data['endonuclease_seed_length'],
|
| 85 |
endonuclease_data['endonuclease_three_prime_length'],
|
| 86 |
organism_code,
|
| 87 |
+
db_path, # This will now always end with a forward slash
|
| 88 |
+
self.settings.get_casper_info_path(),
|
| 89 |
+
file_path,
|
| 90 |
f'{organism_name} {strain}',
|
| 91 |
'notes',
|
| 92 |
f'DATA:{endonuclease_data["endonuclease_on_target_scoring"]}'
|
| 93 |
]
|
| 94 |
+
|
| 95 |
+
# Add logging of the full command
|
| 96 |
+
self.logger.debug(f"Generated command arguments: {arguments}")
|
| 97 |
+
|
| 98 |
return arguments
|
| 99 |
|
| 100 |
def get_arguments_command_for_job(self, job_index):
|
|
|
|
| 106 |
if platform.system() == 'Windows':
|
| 107 |
program = f'"{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Win.exe")}" '
|
| 108 |
elif platform.system() == 'Linux':
|
| 109 |
+
program = f'{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Lin")}'
|
| 110 |
else:
|
| 111 |
program = f'{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Mac")}'
|
| 112 |
return program
|
|
|
|
| 148 |
if not endonuclease:
|
| 149 |
return None
|
| 150 |
return self.endonucleases.get(endonuclease, None)
|
| 151 |
+
|
| 152 |
+
def get_job_name(self, job_index):
|
| 153 |
+
"""Get the name of the job at the given index"""
|
| 154 |
+
if 0 <= job_index < len(self.jobs):
|
| 155 |
+
job_entry = self.jobs[job_index]
|
| 156 |
+
return next(iter(job_entry)) # Returns the first (and only) key
|
| 157 |
+
return None
|
|
@@ -1,8 +1,4 @@
|
|
| 1 |
DETAILED OUTPUT
|
| 2 |
-
CACTTATGACCGGGCAACTT:0.000000
|
| 3 |
-
ACACTTATGACCGGGCAACT:0.080598
|
| 4 |
-
0.080598,1,-100695,ACAATTACGCCCGGGCAACC
|
| 5 |
-
TCAAAATAGCCCAAGTTGCC:0.000000
|
| 6 |
ATTTTGCTACACTTATGACC:0.000000
|
| 7 |
AATTTTGCTACACTTATGAC:0.000000
|
| 8 |
GGGAATACTCCCTTTTATTG:0.000000
|
|
|
|
| 1 |
DETAILED OUTPUT
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
ATTTTGCTACACTTATGACC:0.000000
|
| 3 |
AATTTTGCTACACTTATGAC:0.000000
|
| 4 |
GGGAATACTCCCTTTTATTG:0.000000
|
|
@@ -15,9 +15,13 @@ class StartupWindowModel(QObject):
|
|
| 15 |
return self.settings.get_db_path()
|
| 16 |
|
| 17 |
def save_db_path(self, directory_path):
|
|
|
|
|
|
|
|
|
|
| 18 |
success, message = self.settings.save_db_path(directory_path)
|
| 19 |
-
|
| 20 |
|
| 21 |
def on_db_state_updated(self, is_valid, message, cspr_files):
|
|
|
|
| 22 |
self.logger.debug(f"StartupWindowModel received db state update: valid={is_valid}, message={message}, cspr_files_count={len(cspr_files)}")
|
| 23 |
self.db_state_updated.emit(is_valid, message, cspr_files)
|
|
|
|
| 15 |
return self.settings.get_db_path()
|
| 16 |
|
| 17 |
def save_db_path(self, directory_path):
|
| 18 |
+
"""Save the database path and trigger validation"""
|
| 19 |
+
self.logger.debug(f"Saving database path: {directory_path}")
|
| 20 |
+
# The db_manager will emit its own signals that we're now listening to
|
| 21 |
success, message = self.settings.save_db_path(directory_path)
|
| 22 |
+
return success, message
|
| 23 |
|
| 24 |
def on_db_state_updated(self, is_valid, message, cspr_files):
|
| 25 |
+
"""Handle database state updates"""
|
| 26 |
self.logger.debug(f"StartupWindowModel received db state update: valid={is_valid}, message={message}, cspr_files_count={len(cspr_files)}")
|
| 27 |
self.db_state_updated.emit(is_valid, message, cspr_files)
|
|
@@ -3,13 +3,8 @@ from models.HomeWindowModel import HomeWindowModel
|
|
| 3 |
from models.AnnotationParser import AnnotationParser
|
| 4 |
import os
|
| 5 |
from Bio import SeqIO
|
| 6 |
-
from Bio.Seq import Seq
|
| 7 |
-
from functools import lru_cache
|
| 8 |
-
import threading
|
| 9 |
from collections import defaultdict
|
| 10 |
-
import re
|
| 11 |
import traceback
|
| 12 |
-
import logging
|
| 13 |
|
| 14 |
class ViewTargetsModel(HomeWindowModel):
|
| 15 |
def __init__(self, global_settings):
|
|
@@ -110,28 +105,15 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 110 |
|
| 111 |
# Initialize guides and genes
|
| 112 |
self.guides = []
|
| 113 |
-
self.available_genes = set()
|
| 114 |
|
| 115 |
# Use a set to track unique guide positions
|
| 116 |
seen_guides = set()
|
| 117 |
|
| 118 |
-
# Create chromosome mapping by counting carets
|
| 119 |
-
chrom_mapping = {}
|
| 120 |
-
chrom_count = 0
|
| 121 |
-
annotation_file = self.global_settings.get_current_annotation_file()
|
| 122 |
-
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 123 |
-
|
| 124 |
-
for record in SeqIO.parse(annotation_path, "genbank"):
|
| 125 |
-
chrom_count += 1
|
| 126 |
-
chrom_mapping[record.id] = str(chrom_count)
|
| 127 |
-
|
| 128 |
batch_guides = defaultdict(list)
|
| 129 |
for target in selected_targets:
|
| 130 |
-
#
|
| 131 |
-
|
| 132 |
-
chrom = chrom_mapping.get(target['full_chromosome'], target['chromosome'])
|
| 133 |
-
else:
|
| 134 |
-
chrom = target['chromosome']
|
| 135 |
|
| 136 |
start, end = map(int, target['location'].split('-'))
|
| 137 |
|
|
@@ -147,11 +129,13 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 147 |
'start': start,
|
| 148 |
'end': end
|
| 149 |
})
|
| 150 |
-
|
|
|
|
| 151 |
|
| 152 |
# Process guides by chromosome
|
| 153 |
unique_guides = {} # Use dict to track unique guides by sequence
|
| 154 |
for chrom, guides in batch_guides.items():
|
|
|
|
| 155 |
results = self.cspr_parser.read_targets_batch(chrom, guides, endonuclease)
|
| 156 |
|
| 157 |
# Add feature_id to each result and deduplicate
|
|
@@ -172,6 +156,7 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 172 |
self.guides = list(unique_guides.values())
|
| 173 |
|
| 174 |
self.logger.debug(f"Found {len(self.guides)} unique guides")
|
|
|
|
| 175 |
|
| 176 |
except Exception as e:
|
| 177 |
self.logger.error(f"Error in load_guides: {str(e)}")
|
|
@@ -252,79 +237,77 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 252 |
self.logger.error(f"Error getting available genes: {str(e)}")
|
| 253 |
return []
|
| 254 |
|
| 255 |
-
def _process_guide(self, guide):
|
| 256 |
-
"""Process a single guide - moved to separate method for parallel processing"""
|
| 257 |
-
try:
|
| 258 |
-
# Your existing guide processing logic here
|
| 259 |
-
# Make sure to handle any shared resources thread-safely
|
| 260 |
-
pass
|
| 261 |
-
except Exception as e:
|
| 262 |
-
logging.error(f"Error processing guide: {e}")
|
| 263 |
-
return None
|
| 264 |
-
|
| 265 |
def get_gene_sequence(self, identifier):
|
| 266 |
"""Get gene sequence with optimized caching and minimal I/O"""
|
| 267 |
try:
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
-
#
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
else:
|
| 304 |
-
# Regular gene-based search
|
| 305 |
-
self.logger.debug(f"Getting gene data for locus tag: {identifier}")
|
| 306 |
-
gene_data = self.get_gene_data(identifier)
|
| 307 |
-
if not gene_data or 'info' not in gene_data:
|
| 308 |
-
self.logger.warning(f"No gene data found for locus tag: {identifier}")
|
| 309 |
-
return None
|
| 310 |
-
|
| 311 |
-
# Parse location string (format: "start:end(strand)")
|
| 312 |
-
location = gene_data['info']['location']
|
| 313 |
-
if ':' not in location:
|
| 314 |
-
self.logger.warning(f"Invalid location format: {location}")
|
| 315 |
-
return None
|
| 316 |
-
|
| 317 |
-
# Extract start and end positions
|
| 318 |
-
start = int(location.split(':')[0])
|
| 319 |
-
end = int(location.split(':')[1].split('(')[0])
|
| 320 |
-
|
| 321 |
-
# Get sequence from gene_data directly if available
|
| 322 |
-
if 'sequence' in gene_data:
|
| 323 |
-
sequence = gene_data['sequence']
|
| 324 |
-
self.logger.debug(f"Got sequence of length: {len(sequence)}")
|
| 325 |
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
padding = 30
|
|
|
|
|
|
|
| 328 |
padded_start = max(0, start - padding)
|
| 329 |
padded_end = min(len(sequence), end + padding)
|
| 330 |
|
|
@@ -335,24 +318,23 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 335 |
|
| 336 |
# Combine parts
|
| 337 |
formatted_sequence = five_prime_pad + main_sequence + three_prime_pad
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
self._sequence_cache[cache_key] = result
|
| 349 |
-
|
| 350 |
-
self.logger.debug(f"Retrieved and cached sequence for locus tag {identifier} ({len(formatted_sequence)} bp)")
|
| 351 |
-
return result
|
| 352 |
-
|
| 353 |
-
self.logger.warning(f"No sequence data found in gene_data for {identifier}")
|
| 354 |
-
return None
|
| 355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
except Exception as e:
|
| 357 |
self.logger.error(f"Error getting gene sequence: {str(e)}")
|
| 358 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
@@ -367,28 +349,9 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 367 |
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 368 |
self.annotation_parser.set_annotation_file(annotation_path)
|
| 369 |
|
| 370 |
-
# Get the full chromosome ID by counting carets in annotation file
|
| 371 |
-
full_chrom = None
|
| 372 |
-
chrom_count = 0
|
| 373 |
-
|
| 374 |
-
try:
|
| 375 |
-
for record in SeqIO.parse(self.annotation_path, "genbank"):
|
| 376 |
-
chrom_count += 1
|
| 377 |
-
if chrom_count == int(chrom): # Match based on position rather than ID number
|
| 378 |
-
full_chrom = record.id
|
| 379 |
-
self.logger.debug(f"Found chromosome {chrom} as {full_chrom}")
|
| 380 |
-
break
|
| 381 |
-
except Exception as e:
|
| 382 |
-
self.logger.error(f"Error finding chromosome by position: {str(e)}")
|
| 383 |
-
return None
|
| 384 |
-
|
| 385 |
-
if not full_chrom:
|
| 386 |
-
self.logger.warning(f"Could not find chromosome at position {chrom}")
|
| 387 |
-
return None
|
| 388 |
-
|
| 389 |
feature_info = {
|
| 390 |
-
'chromosome':
|
| 391 |
-
'start': start-1,
|
| 392 |
'end': end
|
| 393 |
}
|
| 394 |
|
|
@@ -449,12 +412,13 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 449 |
if not gene_data or 'info' not in gene_data:
|
| 450 |
self.logger.warning(f"No gene data found for identifier: {identifier}")
|
| 451 |
return None
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
|
|
|
| 455 |
|
| 456 |
# Use _get_sequence_for_position to get sequence with padding
|
| 457 |
-
sequence = self._get_sequence_for_position(
|
| 458 |
if sequence:
|
| 459 |
result = {
|
| 460 |
'sequence': sequence,
|
|
@@ -469,4 +433,89 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 469 |
except Exception as e:
|
| 470 |
self.logger.error(f"Error getting gene sequence for range: {str(e)}")
|
| 471 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 472 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from models.AnnotationParser import AnnotationParser
|
| 4 |
import os
|
| 5 |
from Bio import SeqIO
|
|
|
|
|
|
|
|
|
|
| 6 |
from collections import defaultdict
|
|
|
|
| 7 |
import traceback
|
|
|
|
| 8 |
|
| 9 |
class ViewTargetsModel(HomeWindowModel):
|
| 10 |
def __init__(self, global_settings):
|
|
|
|
| 105 |
|
| 106 |
# Initialize guides and genes
|
| 107 |
self.guides = []
|
| 108 |
+
self.available_genes = set() # Clear existing genes
|
| 109 |
|
| 110 |
# Use a set to track unique guide positions
|
| 111 |
seen_guides = set()
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
batch_guides = defaultdict(list)
|
| 114 |
for target in selected_targets:
|
| 115 |
+
# Use full_chromosome directly if available, otherwise use chromosome
|
| 116 |
+
chrom = target.get('full_chromosome', target['chromosome'])
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
start, end = map(int, target['location'].split('-'))
|
| 119 |
|
|
|
|
| 129 |
'start': start,
|
| 130 |
'end': end
|
| 131 |
})
|
| 132 |
+
# Add only the feature_id from the original target
|
| 133 |
+
self.available_genes.add(target['feature_id'])
|
| 134 |
|
| 135 |
# Process guides by chromosome
|
| 136 |
unique_guides = {} # Use dict to track unique guides by sequence
|
| 137 |
for chrom, guides in batch_guides.items():
|
| 138 |
+
# Use full chromosome ID for CSPR lookup
|
| 139 |
results = self.cspr_parser.read_targets_batch(chrom, guides, endonuclease)
|
| 140 |
|
| 141 |
# Add feature_id to each result and deduplicate
|
|
|
|
| 156 |
self.guides = list(unique_guides.values())
|
| 157 |
|
| 158 |
self.logger.debug(f"Found {len(self.guides)} unique guides")
|
| 159 |
+
self.logger.debug(f"Available genes: {self.available_genes}")
|
| 160 |
|
| 161 |
except Exception as e:
|
| 162 |
self.logger.error(f"Error in load_guides: {str(e)}")
|
|
|
|
| 237 |
self.logger.error(f"Error getting available genes: {str(e)}")
|
| 238 |
return []
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
def get_gene_sequence(self, identifier):
|
| 241 |
"""Get gene sequence with optimized caching and minimal I/O"""
|
| 242 |
try:
|
| 243 |
+
self.logger.debug(f"Getting gene sequence for identifier: {identifier}")
|
| 244 |
+
self.logger.debug(f"View exons only is: {getattr(self, '_view_exons_only', False)}")
|
| 245 |
+
|
| 246 |
+
# Regular gene-based search
|
| 247 |
+
self.logger.debug(f"Getting gene data for locus tag: {identifier}")
|
| 248 |
+
gene_data = self.get_gene_data(identifier)
|
| 249 |
+
if not gene_data or 'info' not in gene_data:
|
| 250 |
+
self.logger.warning(f"No gene data found for locus tag: {identifier}")
|
| 251 |
+
return None
|
| 252 |
+
|
| 253 |
+
# Check if we're in exons-only mode
|
| 254 |
+
if getattr(self, '_view_exons_only', False):
|
| 255 |
+
print(f"gene_data: {gene_data}")
|
| 256 |
+
full_location = gene_data['info'].get('full_location', '')
|
| 257 |
+
print(f"Full location: {full_location}")
|
| 258 |
+
if full_location and ',' in full_location: # Multiple exons
|
| 259 |
+
self.logger.debug(f"Processing exons from full location: {full_location}")
|
| 260 |
+
exon_sequences = []
|
| 261 |
+
full_sequence = gene_data['sequence'] # Use sequence from gene_data
|
| 262 |
|
| 263 |
+
# Calculate padding offset
|
| 264 |
+
padding = 30
|
| 265 |
+
gene_start = gene_data['info']['start']
|
| 266 |
+
padded_start = max(0, gene_start - padding)
|
| 267 |
+
padding_offset = gene_start - padded_start
|
| 268 |
+
|
| 269 |
+
print(f"gene_start: {gene_start}, padded_start: {padded_start}, padding_offset: {padding_offset}")
|
| 270 |
+
|
| 271 |
+
# Process each exon location
|
| 272 |
+
for exon in full_location.split(','):
|
| 273 |
+
# Extract coordinates and strand
|
| 274 |
+
coords = exon.split('(')[0] # Get part before strand
|
| 275 |
+
strand = exon.split('(')[1][0] # Get + or - from (+ or (-
|
| 276 |
+
start, end = map(int, coords.split('..'))
|
| 277 |
+
|
| 278 |
+
print(f"coords: {coords}, strand: {strand}, start: {start}, end: {end}")
|
| 279 |
+
|
| 280 |
+
# Adjust coordinates relative to gene start and account for padding
|
| 281 |
+
relative_start = start - gene_start + padding_offset
|
| 282 |
+
relative_end = end - gene_start + padding_offset
|
| 283 |
+
print(f"relative_start: {relative_start}, relative_end: {relative_end}")
|
| 284 |
|
| 285 |
+
# Get exon sequence from the padded sequence
|
| 286 |
+
exon_seq = full_sequence[relative_start:relative_end]
|
| 287 |
+
|
| 288 |
+
exon_sequences.append(exon_seq)
|
| 289 |
|
| 290 |
+
# Join exon sequences
|
| 291 |
+
sequence = ''.join(exon_sequences)
|
| 292 |
+
self.logger.debug(f"Created concatenated exon sequence of length: {len(sequence)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
+
return {
|
| 295 |
+
'sequence': sequence,
|
| 296 |
+
'info': gene_data['info'],
|
| 297 |
+
'start': gene_data['info']['start'],
|
| 298 |
+
'end': gene_data['info']['end']
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
# If not in exons-only mode or no exons to process, return normal sequence
|
| 302 |
+
if 'sequence' in gene_data:
|
| 303 |
+
sequence = gene_data['sequence']
|
| 304 |
+
self.logger.debug(f"Got sequence of length: {len(sequence)}")
|
| 305 |
+
|
| 306 |
+
# Format sequence with padding in lowercase (only if not in exons-only mode)
|
| 307 |
+
if not hasattr(self, '_view_exons_only') or not self._view_exons_only:
|
| 308 |
padding = 30
|
| 309 |
+
start = gene_data['info']['start']
|
| 310 |
+
end = gene_data['info']['end']
|
| 311 |
padded_start = max(0, start - padding)
|
| 312 |
padded_end = min(len(sequence), end + padding)
|
| 313 |
|
|
|
|
| 318 |
|
| 319 |
# Combine parts
|
| 320 |
formatted_sequence = five_prime_pad + main_sequence + three_prime_pad
|
| 321 |
+
else:
|
| 322 |
+
formatted_sequence = sequence
|
| 323 |
+
|
| 324 |
+
result = {
|
| 325 |
+
'sequence': formatted_sequence,
|
| 326 |
+
'info': gene_data['info'],
|
| 327 |
+
'start': gene_data['info']['start'],
|
| 328 |
+
'end': gene_data['info']['end'],
|
| 329 |
+
'full_location': gene_data['info'].get('full_location', '')
|
| 330 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
+
self.logger.debug(f"Returning sequence of length: {len(formatted_sequence)}")
|
| 333 |
+
return result
|
| 334 |
+
|
| 335 |
+
self.logger.warning(f"No sequence data found in gene_data for {identifier}")
|
| 336 |
+
return None
|
| 337 |
+
|
| 338 |
except Exception as e:
|
| 339 |
self.logger.error(f"Error getting gene sequence: {str(e)}")
|
| 340 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
|
|
| 349 |
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 350 |
self.annotation_parser.set_annotation_file(annotation_path)
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
feature_info = {
|
| 353 |
+
'chromosome': chrom, # Use raw chromosome ID directly
|
| 354 |
+
'start': start-1, # Convert to 0-based indexing
|
| 355 |
'end': end
|
| 356 |
}
|
| 357 |
|
|
|
|
| 412 |
if not gene_data or 'info' not in gene_data:
|
| 413 |
self.logger.warning(f"No gene data found for identifier: {identifier}")
|
| 414 |
return None
|
| 415 |
+
|
| 416 |
+
chrom = gene_data['info']['chromosome']
|
| 417 |
+
|
| 418 |
+
self.logger.debug(f"Getting sequence for chromosome: {chrom}, start: {start}, end: {end}")
|
| 419 |
|
| 420 |
# Use _get_sequence_for_position to get sequence with padding
|
| 421 |
+
sequence = self._get_sequence_for_position(chrom, start, end)
|
| 422 |
if sequence:
|
| 423 |
result = {
|
| 424 |
'sequence': sequence,
|
|
|
|
| 433 |
except Exception as e:
|
| 434 |
self.logger.error(f"Error getting gene sequence for range: {str(e)}")
|
| 435 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 436 |
+
return None
|
| 437 |
+
|
| 438 |
+
def set_view_exons_only(self, enabled):
|
| 439 |
+
"""Set whether to view exons only"""
|
| 440 |
+
try:
|
| 441 |
+
self.logger.debug(f"Setting view exons only to: {enabled}")
|
| 442 |
+
self._view_exons_only = enabled
|
| 443 |
+
# Clear cache when changing view mode
|
| 444 |
+
self._sequence_cache.clear()
|
| 445 |
+
self.logger.debug("Cleared sequence cache")
|
| 446 |
+
except Exception as e:
|
| 447 |
+
self.logger.error(f"Error setting view exons only: {str(e)}")
|
| 448 |
+
|
| 449 |
+
def get_features_for_gene(self, locus_tag):
|
| 450 |
+
"""Get features for a specific gene"""
|
| 451 |
+
try:
|
| 452 |
+
if not self.annotation_parser:
|
| 453 |
+
self._initialize_annotation_parser()
|
| 454 |
+
|
| 455 |
+
features = []
|
| 456 |
+
gene_data = self.get_gene_data(locus_tag)
|
| 457 |
+
|
| 458 |
+
if gene_data and 'info' in gene_data:
|
| 459 |
+
info = gene_data['info']
|
| 460 |
+
|
| 461 |
+
# Add the main gene feature
|
| 462 |
+
features.append({
|
| 463 |
+
'type': info['feature_type'],
|
| 464 |
+
'start': info['start'],
|
| 465 |
+
'end': info['end'],
|
| 466 |
+
'name': info['gene_name'],
|
| 467 |
+
'id': locus_tag,
|
| 468 |
+
'strand': '+' if '(+)' in info['location'] else '-'
|
| 469 |
+
})
|
| 470 |
+
|
| 471 |
+
# Parse additional features from full location if available
|
| 472 |
+
if 'full_location' in info and ',' in info['full_location']:
|
| 473 |
+
for i, part in enumerate(info['full_location'].split(',')):
|
| 474 |
+
coords = part.split('(')[0]
|
| 475 |
+
strand = part.split('(')[1][0]
|
| 476 |
+
start, end = map(int, coords.split('..'))
|
| 477 |
+
|
| 478 |
+
features.append({
|
| 479 |
+
'type': 'exon',
|
| 480 |
+
'start': start,
|
| 481 |
+
'end': end,
|
| 482 |
+
'name': f'Exon {i+1}',
|
| 483 |
+
'id': f'{locus_tag}_exon_{i+1}',
|
| 484 |
+
'strand': strand
|
| 485 |
+
})
|
| 486 |
+
|
| 487 |
+
return features
|
| 488 |
+
|
| 489 |
+
except Exception as e:
|
| 490 |
+
self.logger.error(f"Error getting features for gene: {str(e)}")
|
| 491 |
+
return []
|
| 492 |
+
|
| 493 |
+
def get_features_for_region(self, chromosome, start, end):
|
| 494 |
+
"""Get features within a specific region"""
|
| 495 |
+
try:
|
| 496 |
+
if not self.annotation_parser:
|
| 497 |
+
self._initialize_annotation_parser()
|
| 498 |
+
|
| 499 |
+
features = []
|
| 500 |
+
|
| 501 |
+
# Search through index for features in this region
|
| 502 |
+
if hasattr(self, '_index') and 'locus_tags' in self._index:
|
| 503 |
+
for locus_tag, feature_info in self._index['locus_tags'].items():
|
| 504 |
+
if (feature_info['chromosome'] == chromosome and
|
| 505 |
+
feature_info['start'] <= end and
|
| 506 |
+
feature_info['end'] >= start):
|
| 507 |
+
|
| 508 |
+
features.append({
|
| 509 |
+
'type': feature_info['feature_type'],
|
| 510 |
+
'start': feature_info['start'],
|
| 511 |
+
'end': feature_info['end'],
|
| 512 |
+
'name': feature_info['gene_name'],
|
| 513 |
+
'id': locus_tag,
|
| 514 |
+
'strand': '+' if '(+)' in feature_info['location'] else '-'
|
| 515 |
+
})
|
| 516 |
+
|
| 517 |
+
return features
|
| 518 |
+
|
| 519 |
+
except Exception as e:
|
| 520 |
+
self.logger.error(f"Error getting features for region: {str(e)}")
|
| 521 |
+
return []
|
|
@@ -70,8 +70,8 @@
|
|
| 70 |
<string>Step 1: Input Search Options</string>
|
| 71 |
</property>
|
| 72 |
<layout class="QGridLayout" name="gridLayout_4">
|
| 73 |
-
<item row="
|
| 74 |
-
<widget class="QLabel" name="
|
| 75 |
<property name="sizePolicy">
|
| 76 |
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
| 77 |
<horstretch>0</horstretch>
|
|
@@ -79,10 +79,10 @@
|
|
| 79 |
</sizepolicy>
|
| 80 |
</property>
|
| 81 |
<property name="toolTip">
|
| 82 |
-
<string><html><head/><body><p><span style=" font-size:12pt;">
|
| 83 |
</property>
|
| 84 |
<property name="text">
|
| 85 |
-
<string>
|
| 86 |
</property>
|
| 87 |
</widget>
|
| 88 |
</item>
|
|
@@ -99,10 +99,10 @@
|
|
| 99 |
</property>
|
| 100 |
</widget>
|
| 101 |
</item>
|
| 102 |
-
<item row="
|
| 103 |
-
<widget class="QLineEdit" name="
|
| 104 |
-
<property name="
|
| 105 |
-
<string>
|
| 106 |
</property>
|
| 107 |
</widget>
|
| 108 |
</item>
|
|
@@ -119,15 +119,15 @@
|
|
| 119 |
</property>
|
| 120 |
</widget>
|
| 121 |
</item>
|
| 122 |
-
<item row="
|
| 123 |
-
<widget class="QLineEdit" name="
|
| 124 |
<property name="placeholderText">
|
| 125 |
-
<string>Ex.
|
| 126 |
</property>
|
| 127 |
</widget>
|
| 128 |
</item>
|
| 129 |
-
<item row="
|
| 130 |
-
<widget class="QLabel" name="
|
| 131 |
<property name="sizePolicy">
|
| 132 |
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
| 133 |
<horstretch>0</horstretch>
|
|
@@ -135,27 +135,37 @@
|
|
| 135 |
</sizepolicy>
|
| 136 |
</property>
|
| 137 |
<property name="toolTip">
|
| 138 |
-
<string><html><head/><body><p><span style=" font-size:12pt;">
|
| 139 |
</property>
|
| 140 |
<property name="text">
|
| 141 |
-
<string>
|
| 142 |
</property>
|
| 143 |
</widget>
|
| 144 |
</item>
|
| 145 |
-
<item row="
|
| 146 |
-
<widget class="
|
| 147 |
-
<property name="
|
| 148 |
-
<string
|
| 149 |
</property>
|
| 150 |
</widget>
|
| 151 |
</item>
|
| 152 |
-
<item row="
|
| 153 |
-
<widget class="
|
| 154 |
<property name="text">
|
| 155 |
-
<string
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
</property>
|
| 157 |
</widget>
|
| 158 |
</item>
|
|
|
|
|
|
|
|
|
|
| 159 |
</layout>
|
| 160 |
</widget>
|
| 161 |
</item>
|
|
|
|
| 70 |
<string>Step 1: Input Search Options</string>
|
| 71 |
</property>
|
| 72 |
<layout class="QGridLayout" name="gridLayout_4">
|
| 73 |
+
<item row="2" column="0">
|
| 74 |
+
<widget class="QLabel" name="lblMaxResults">
|
| 75 |
<property name="sizePolicy">
|
| 76 |
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
| 77 |
<horstretch>0</horstretch>
|
|
|
|
| 79 |
</sizepolicy>
|
| 80 |
</property>
|
| 81 |
<property name="toolTip">
|
| 82 |
+
<string><html><head/><body><p><span style=" font-size:12pt;">Number of search results to return.</span></p></body></html></string>
|
| 83 |
</property>
|
| 84 |
<property name="text">
|
| 85 |
+
<string>Max Results (Default = 100)</string>
|
| 86 |
</property>
|
| 87 |
</widget>
|
| 88 |
</item>
|
|
|
|
| 99 |
</property>
|
| 100 |
</widget>
|
| 101 |
</item>
|
| 102 |
+
<item row="1" column="1">
|
| 103 |
+
<widget class="QLineEdit" name="ledStrain">
|
| 104 |
+
<property name="placeholderText">
|
| 105 |
+
<string>Ex. K-12</string>
|
| 106 |
</property>
|
| 107 |
</widget>
|
| 108 |
</item>
|
|
|
|
| 119 |
</property>
|
| 120 |
</widget>
|
| 121 |
</item>
|
| 122 |
+
<item row="0" column="1">
|
| 123 |
+
<widget class="QLineEdit" name="ledOrganism">
|
| 124 |
<property name="placeholderText">
|
| 125 |
+
<string>Ex. Escherichia coli</string>
|
| 126 |
</property>
|
| 127 |
</widget>
|
| 128 |
</item>
|
| 129 |
+
<item row="4" column="0">
|
| 130 |
+
<widget class="QLabel" name="lblCompleteGenomesOnly">
|
| 131 |
<property name="sizePolicy">
|
| 132 |
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
| 133 |
<horstretch>0</horstretch>
|
|
|
|
| 135 |
</sizepolicy>
|
| 136 |
</property>
|
| 137 |
<property name="toolTip">
|
| 138 |
+
<string><html><head/><body><p><span style=" font-size:12pt;">Only display entries classified as &quot;Complete Genomes&quot; by NCBI.</span></p></body></html></string>
|
| 139 |
</property>
|
| 140 |
<property name="text">
|
| 141 |
+
<string>Complete Genomes Only</string>
|
| 142 |
</property>
|
| 143 |
</widget>
|
| 144 |
</item>
|
| 145 |
+
<item row="4" column="1">
|
| 146 |
+
<widget class="QCheckBox" name="chkCompleteGenomesOnly">
|
| 147 |
+
<property name="text">
|
| 148 |
+
<string/>
|
| 149 |
</property>
|
| 150 |
</widget>
|
| 151 |
</item>
|
| 152 |
+
<item row="2" column="1">
|
| 153 |
+
<widget class="QLineEdit" name="ledMaxResults">
|
| 154 |
<property name="text">
|
| 155 |
+
<string>100</string>
|
| 156 |
+
</property>
|
| 157 |
+
</widget>
|
| 158 |
+
</item>
|
| 159 |
+
<item row="3" column="0">
|
| 160 |
+
<widget class="QLabel" name="lblDatabase">
|
| 161 |
+
<property name="text">
|
| 162 |
+
<string>Database:</string>
|
| 163 |
</property>
|
| 164 |
</widget>
|
| 165 |
</item>
|
| 166 |
+
<item row="3" column="1">
|
| 167 |
+
<widget class="QComboBox" name="cmbDatabase"/>
|
| 168 |
+
</item>
|
| 169 |
</layout>
|
| 170 |
</widget>
|
| 171 |
</item>
|
|
@@ -6,8 +6,8 @@
|
|
| 6 |
<rect>
|
| 7 |
<x>0</x>
|
| 8 |
<y>0</y>
|
| 9 |
-
<width>
|
| 10 |
-
<height>
|
| 11 |
</rect>
|
| 12 |
</property>
|
| 13 |
<property name="font">
|
|
@@ -34,6 +34,16 @@
|
|
| 34 |
<string>Gene Viewer</string>
|
| 35 |
</property>
|
| 36 |
<layout class="QGridLayout" name="gridLayout_6">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
<item row="2" column="0">
|
| 38 |
<widget class="QLabel" name="lblStartLocation">
|
| 39 |
<property name="text">
|
|
@@ -41,8 +51,8 @@
|
|
| 41 |
</property>
|
| 42 |
</widget>
|
| 43 |
</item>
|
| 44 |
-
<item row="
|
| 45 |
-
<widget class="QLineEdit" name="
|
| 46 |
</item>
|
| 47 |
<item row="3" column="0">
|
| 48 |
<widget class="QLabel" name="lblStopLocation">
|
|
@@ -51,16 +61,6 @@
|
|
| 51 |
</property>
|
| 52 |
</widget>
|
| 53 |
</item>
|
| 54 |
-
<item row="3" column="2">
|
| 55 |
-
<widget class="QPushButton" name="pbtnResetLocation">
|
| 56 |
-
<property name="text">
|
| 57 |
-
<string>Reset Location</string>
|
| 58 |
-
</property>
|
| 59 |
-
</widget>
|
| 60 |
-
</item>
|
| 61 |
-
<item row="3" column="1">
|
| 62 |
-
<widget class="QLineEdit" name="ledStopLocation"/>
|
| 63 |
-
</item>
|
| 64 |
<item row="2" column="2">
|
| 65 |
<widget class="QPushButton" name="pbtnChangeLocation">
|
| 66 |
<property name="toolTip">
|
|
@@ -74,6 +74,13 @@
|
|
| 74 |
<item row="5" column="0" colspan="5">
|
| 75 |
<widget class="QTextEdit" name="txtedGeneViewer"/>
|
| 76 |
</item>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
</layout>
|
| 78 |
</widget>
|
| 79 |
</item>
|
|
@@ -89,23 +96,40 @@
|
|
| 89 |
<string>Guide Viewer</string>
|
| 90 |
</property>
|
| 91 |
<layout class="QGridLayout" name="gridLayout_4">
|
| 92 |
-
<item row="
|
| 93 |
-
<widget class="
|
| 94 |
-
<property name="
|
| 95 |
-
<
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
</
|
| 99 |
</property>
|
| 100 |
</widget>
|
| 101 |
</item>
|
| 102 |
-
<item row="
|
| 103 |
-
<widget class="QPushButton" name="
|
| 104 |
<property name="toolTip">
|
| 105 |
-
<string><html><head/><body><p>
|
| 106 |
</property>
|
| 107 |
<property name="text">
|
| 108 |
-
<string>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
</property>
|
| 110 |
</widget>
|
| 111 |
</item>
|
|
@@ -119,29 +143,20 @@
|
|
| 119 |
</property>
|
| 120 |
</widget>
|
| 121 |
</item>
|
| 122 |
-
<item row="
|
| 123 |
-
<widget class="
|
| 124 |
-
<property name="toolTip">
|
| 125 |
-
<string><html><head/><body><p><span style=" font-size:12pt;">This button clears all highlighted guides from the gene viewer box.</span></p></body></html></string>
|
| 126 |
-
</property>
|
| 127 |
<property name="text">
|
| 128 |
-
<string>
|
| 129 |
</property>
|
| 130 |
</widget>
|
| 131 |
</item>
|
| 132 |
-
<item row="
|
| 133 |
-
<widget class="
|
| 134 |
<property name="text">
|
| 135 |
-
<string>
|
| 136 |
</property>
|
| 137 |
</widget>
|
| 138 |
</item>
|
| 139 |
-
<item row="11" column="0" colspan="4">
|
| 140 |
-
<widget class="QTableWidget" name="tblGuides"/>
|
| 141 |
-
</item>
|
| 142 |
-
<item row="6" column="3">
|
| 143 |
-
<widget class="QSpinBox" name="spnMinOTScore"/>
|
| 144 |
-
</item>
|
| 145 |
<item row="5" column="0">
|
| 146 |
<widget class="QLabel" name="lblEndonuclease">
|
| 147 |
<property name="text">
|
|
@@ -149,27 +164,39 @@
|
|
| 149 |
</property>
|
| 150 |
</widget>
|
| 151 |
</item>
|
| 152 |
-
<item row="
|
| 153 |
-
<widget class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
<property name="text">
|
| 155 |
-
<string>
|
| 156 |
</property>
|
| 157 |
</widget>
|
| 158 |
</item>
|
| 159 |
-
<item row="
|
| 160 |
-
<widget class="
|
| 161 |
-
<property name="
|
| 162 |
-
<
|
|
|
|
|
|
|
|
|
|
| 163 |
</property>
|
| 164 |
</widget>
|
| 165 |
</item>
|
| 166 |
-
<item row="
|
| 167 |
-
<widget class="
|
| 168 |
<property name="text">
|
| 169 |
-
<string>
|
| 170 |
</property>
|
| 171 |
</widget>
|
| 172 |
</item>
|
|
|
|
|
|
|
|
|
|
| 173 |
<item row="4" column="0">
|
| 174 |
<widget class="QLabel" name="lblGene">
|
| 175 |
<property name="text">
|
|
@@ -177,33 +204,13 @@
|
|
| 177 |
</property>
|
| 178 |
</widget>
|
| 179 |
</item>
|
| 180 |
-
<item row="
|
| 181 |
-
<widget class="QPushButton" name="
|
| 182 |
-
<property name="toolTip">
|
| 183 |
-
<string><html><head/><body><p><span style=" font-size:12pt;">Perform off-target analysis on the selected guides.</span></p></body></html></string>
|
| 184 |
-
</property>
|
| 185 |
-
<property name="text">
|
| 186 |
-
<string>Off-Target</string>
|
| 187 |
-
</property>
|
| 188 |
-
</widget>
|
| 189 |
-
</item>
|
| 190 |
-
<item row="12" column="2">
|
| 191 |
-
<widget class="QPushButton" name="pbtnCoTargeting">
|
| 192 |
-
<property name="toolTip">
|
| 193 |
-
<string><html><head/><body><p>Determine guides with synergistic PAMs (Ex. saCas9 and spCas9 compatible guides). <span style=" font-weight:600;">Note:</span> to analyze an organism for co-targeting guides, separate CSPR files must be generated for each additional endonuclease. Co-targeting endonucleases must have the same PAM directionality, same total gRNA length, and overlapping PAM sequences to be compatible.</p></body></html></string>
|
| 194 |
-
</property>
|
| 195 |
-
<property name="text">
|
| 196 |
-
<string>Co-Targeting</string>
|
| 197 |
-
</property>
|
| 198 |
-
</widget>
|
| 199 |
-
</item>
|
| 200 |
-
<item row="12" column="3">
|
| 201 |
-
<widget class="QPushButton" name="pbtnExportSelectedgRNAs">
|
| 202 |
<property name="toolTip">
|
| 203 |
-
<string><html><head/><body><p><span style=" font-size:12pt;">
|
| 204 |
</property>
|
| 205 |
<property name="text">
|
| 206 |
-
<string>
|
| 207 |
</property>
|
| 208 |
</widget>
|
| 209 |
</item>
|
|
|
|
| 6 |
<rect>
|
| 7 |
<x>0</x>
|
| 8 |
<y>0</y>
|
| 9 |
+
<width>1920</width>
|
| 10 |
+
<height>1147</height>
|
| 11 |
</rect>
|
| 12 |
</property>
|
| 13 |
<property name="font">
|
|
|
|
| 34 |
<string>Gene Viewer</string>
|
| 35 |
</property>
|
| 36 |
<layout class="QGridLayout" name="gridLayout_6">
|
| 37 |
+
<item row="2" column="1">
|
| 38 |
+
<widget class="QLineEdit" name="ledStartLocation"/>
|
| 39 |
+
</item>
|
| 40 |
+
<item row="3" column="2">
|
| 41 |
+
<widget class="QPushButton" name="pbtnResetLocation">
|
| 42 |
+
<property name="text">
|
| 43 |
+
<string>Reset Location</string>
|
| 44 |
+
</property>
|
| 45 |
+
</widget>
|
| 46 |
+
</item>
|
| 47 |
<item row="2" column="0">
|
| 48 |
<widget class="QLabel" name="lblStartLocation">
|
| 49 |
<property name="text">
|
|
|
|
| 51 |
</property>
|
| 52 |
</widget>
|
| 53 |
</item>
|
| 54 |
+
<item row="3" column="1">
|
| 55 |
+
<widget class="QLineEdit" name="ledStopLocation"/>
|
| 56 |
</item>
|
| 57 |
<item row="3" column="0">
|
| 58 |
<widget class="QLabel" name="lblStopLocation">
|
|
|
|
| 61 |
</property>
|
| 62 |
</widget>
|
| 63 |
</item>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<item row="2" column="2">
|
| 65 |
<widget class="QPushButton" name="pbtnChangeLocation">
|
| 66 |
<property name="toolTip">
|
|
|
|
| 74 |
<item row="5" column="0" colspan="5">
|
| 75 |
<widget class="QTextEdit" name="txtedGeneViewer"/>
|
| 76 |
</item>
|
| 77 |
+
<item row="4" column="2">
|
| 78 |
+
<widget class="QCheckBox" name="chkViewExonsOnly">
|
| 79 |
+
<property name="text">
|
| 80 |
+
<string>View Exons Only</string>
|
| 81 |
+
</property>
|
| 82 |
+
</widget>
|
| 83 |
+
</item>
|
| 84 |
</layout>
|
| 85 |
</widget>
|
| 86 |
</item>
|
|
|
|
| 96 |
<string>Guide Viewer</string>
|
| 97 |
</property>
|
| 98 |
<layout class="QGridLayout" name="gridLayout_4">
|
| 99 |
+
<item row="12" column="2">
|
| 100 |
+
<widget class="QPushButton" name="pbtnCoTargeting">
|
| 101 |
+
<property name="toolTip">
|
| 102 |
+
<string><html><head/><body><p>Determine guides with synergistic PAMs (Ex. saCas9 and spCas9 compatible guides). <span style=" font-weight:600;">Note:</span> to analyze an organism for co-targeting guides, separate CSPR files must be generated for each additional endonuclease. Co-targeting endonucleases must have the same PAM directionality, same total gRNA length, and overlapping PAM sequences to be compatible.</p></body></html></string>
|
| 103 |
+
</property>
|
| 104 |
+
<property name="text">
|
| 105 |
+
<string>Co-Targeting</string>
|
| 106 |
</property>
|
| 107 |
</widget>
|
| 108 |
</item>
|
| 109 |
+
<item row="12" column="1">
|
| 110 |
+
<widget class="QPushButton" name="pbtnOffTarget">
|
| 111 |
<property name="toolTip">
|
| 112 |
+
<string><html><head/><body><p><span style=" font-size:12pt;">Perform off-target analysis on the selected guides.</span></p></body></html></string>
|
| 113 |
</property>
|
| 114 |
<property name="text">
|
| 115 |
+
<string>Off-Target</string>
|
| 116 |
+
</property>
|
| 117 |
+
</widget>
|
| 118 |
+
</item>
|
| 119 |
+
<item row="12" column="3">
|
| 120 |
+
<widget class="QPushButton" name="pbtnExportSelectedgRNAs">
|
| 121 |
+
<property name="toolTip">
|
| 122 |
+
<string><html><head/><body><p><span style=" font-size:12pt;">Export selected guides to a CSV file.</span></p></body></html></string>
|
| 123 |
+
</property>
|
| 124 |
+
<property name="text">
|
| 125 |
+
<string>Export Selected gRNAs</string>
|
| 126 |
+
</property>
|
| 127 |
+
</widget>
|
| 128 |
+
</item>
|
| 129 |
+
<item row="10" column="0">
|
| 130 |
+
<widget class="QCheckBox" name="chkSelectAll">
|
| 131 |
+
<property name="text">
|
| 132 |
+
<string>Select All</string>
|
| 133 |
</property>
|
| 134 |
</widget>
|
| 135 |
</item>
|
|
|
|
| 143 |
</property>
|
| 144 |
</widget>
|
| 145 |
</item>
|
| 146 |
+
<item row="6" column="0">
|
| 147 |
+
<widget class="QCheckBox" name="chkFilter5PrimeG">
|
|
|
|
|
|
|
|
|
|
| 148 |
<property name="text">
|
| 149 |
+
<string>Filter 5' G Sequences</string>
|
| 150 |
</property>
|
| 151 |
</widget>
|
| 152 |
</item>
|
| 153 |
+
<item row="12" column="0">
|
| 154 |
+
<widget class="QPushButton" name="pbtnScoringOptions">
|
| 155 |
<property name="text">
|
| 156 |
+
<string>Scoring Options</string>
|
| 157 |
</property>
|
| 158 |
</widget>
|
| 159 |
</item>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
<item row="5" column="0">
|
| 161 |
<widget class="QLabel" name="lblEndonuclease">
|
| 162 |
<property name="text">
|
|
|
|
| 164 |
</property>
|
| 165 |
</widget>
|
| 166 |
</item>
|
| 167 |
+
<item row="6" column="3">
|
| 168 |
+
<widget class="QSpinBox" name="spnMinOTScore"/>
|
| 169 |
+
</item>
|
| 170 |
+
<item row="10" column="2">
|
| 171 |
+
<widget class="QPushButton" name="pbtnHighlightGuides">
|
| 172 |
+
<property name="toolTip">
|
| 173 |
+
<string><html><head/><body><p>This button will highlight the sequences in the Gene Viewer that match the sequences selected in the table.</p></body></html></string>
|
| 174 |
+
</property>
|
| 175 |
<property name="text">
|
| 176 |
+
<string>Highlight Guides</string>
|
| 177 |
</property>
|
| 178 |
</widget>
|
| 179 |
</item>
|
| 180 |
+
<item row="4" column="1" colspan="3">
|
| 181 |
+
<widget class="QComboBox" name="cmbGene">
|
| 182 |
+
<property name="minimumSize">
|
| 183 |
+
<size>
|
| 184 |
+
<width>225</width>
|
| 185 |
+
<height>0</height>
|
| 186 |
+
</size>
|
| 187 |
</property>
|
| 188 |
</widget>
|
| 189 |
</item>
|
| 190 |
+
<item row="6" column="2">
|
| 191 |
+
<widget class="QLabel" name="lblMinOTScore">
|
| 192 |
<property name="text">
|
| 193 |
+
<string>Minimum On-Target Score</string>
|
| 194 |
</property>
|
| 195 |
</widget>
|
| 196 |
</item>
|
| 197 |
+
<item row="11" column="0" colspan="4">
|
| 198 |
+
<widget class="QTableWidget" name="tblGuides"/>
|
| 199 |
+
</item>
|
| 200 |
<item row="4" column="0">
|
| 201 |
<widget class="QLabel" name="lblGene">
|
| 202 |
<property name="text">
|
|
|
|
| 204 |
</property>
|
| 205 |
</widget>
|
| 206 |
</item>
|
| 207 |
+
<item row="10" column="3">
|
| 208 |
+
<widget class="QPushButton" name="pbtnClearGuides">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
<property name="toolTip">
|
| 210 |
+
<string><html><head/><body><p><span style=" font-size:12pt;">This button clears all highlighted guides from the gene viewer box.</span></p></body></html></string>
|
| 211 |
</property>
|
| 212 |
<property name="text">
|
| 213 |
+
<string>Clear Guides</string>
|
| 214 |
</property>
|
| 215 |
</widget>
|
| 216 |
</item>
|
|
@@ -65,8 +65,8 @@ class CloseableTabWidget(QTabWidget):
|
|
| 65 |
# Add the tab
|
| 66 |
index = super().addTab(widget, label)
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
close_button = self._create_close_button(index, label)
|
| 71 |
self._tabs[tab_id]['close_button'] = close_button
|
| 72 |
self.tabBar().setTabButton(index, QTabBar.ButtonPosition.RightSide, close_button)
|
|
@@ -78,25 +78,37 @@ class CloseableTabWidget(QTabWidget):
|
|
| 78 |
|
| 79 |
def _create_close_button(self, index, label):
|
| 80 |
"""Create a new close button for a tab"""
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
def safely_close_tab(self, index):
|
| 102 |
"""Safely handle tab closure with error checking"""
|
|
@@ -118,14 +130,15 @@ class CloseableTabWidget(QTabWidget):
|
|
| 118 |
def _update_all_tabs(self):
|
| 119 |
"""Update all tabs and their close buttons"""
|
| 120 |
try:
|
| 121 |
-
for i in range(
|
| 122 |
widget = self.widget(i)
|
| 123 |
if widget:
|
| 124 |
label = self.tabText(i)
|
| 125 |
tab_id = f"{label}_{id(widget)}"
|
| 126 |
|
| 127 |
-
# Create new close button if needed
|
| 128 |
-
if
|
|
|
|
| 129 |
close_button = self._create_close_button(i, label)
|
| 130 |
self._tabs[tab_id] = {
|
| 131 |
'widget': widget,
|
|
@@ -133,7 +146,7 @@ class CloseableTabWidget(QTabWidget):
|
|
| 133 |
'close_button': close_button
|
| 134 |
}
|
| 135 |
self.tabBar().setTabButton(i, QTabBar.ButtonPosition.RightSide, close_button)
|
| 136 |
-
|
| 137 |
# Update existing close button's click connection
|
| 138 |
close_button = self._tabs[tab_id]['close_button']
|
| 139 |
close_button.clicked.disconnect()
|
|
|
|
| 65 |
# Add the tab
|
| 66 |
index = super().addTab(widget, label)
|
| 67 |
|
| 68 |
+
# Create and setup close button for all tabs except Home and Startup
|
| 69 |
+
if label not in ["Home", "Startup"]:
|
| 70 |
close_button = self._create_close_button(index, label)
|
| 71 |
self._tabs[tab_id]['close_button'] = close_button
|
| 72 |
self.tabBar().setTabButton(index, QTabBar.ButtonPosition.RightSide, close_button)
|
|
|
|
| 78 |
|
| 79 |
def _create_close_button(self, index, label):
|
| 80 |
"""Create a new close button for a tab"""
|
| 81 |
+
try:
|
| 82 |
+
close_button = QToolButton(self.tabBar())
|
| 83 |
+
close_button.setObjectName(f"close_button_{label}")
|
| 84 |
+
|
| 85 |
+
# Always use a custom close icon style
|
| 86 |
+
close_button.setText("×") # Using multiplication symbol as close icon
|
| 87 |
+
close_button.setStyleSheet("""
|
| 88 |
+
QToolButton {
|
| 89 |
+
border: none;
|
| 90 |
+
padding: 0px;
|
| 91 |
+
color: #666666;
|
| 92 |
+
background: transparent;
|
| 93 |
+
font-size: 16px;
|
| 94 |
+
font-weight: bold;
|
| 95 |
+
}
|
| 96 |
+
QToolButton:hover {
|
| 97 |
+
color: #ffffff;
|
| 98 |
+
background: #c42b1c;
|
| 99 |
+
}
|
| 100 |
+
""")
|
| 101 |
+
|
| 102 |
+
close_button.setAutoRaise(True)
|
| 103 |
+
close_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
| 104 |
+
close_button.setFixedSize(20, 20)
|
| 105 |
+
close_button.clicked.connect(lambda checked, idx=index: self.safely_close_tab(idx))
|
| 106 |
+
|
| 107 |
+
return close_button
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
self.logger.error(f"Error creating close button: {e}")
|
| 111 |
+
raise
|
| 112 |
|
| 113 |
def safely_close_tab(self, index):
|
| 114 |
"""Safely handle tab closure with error checking"""
|
|
|
|
| 130 |
def _update_all_tabs(self):
|
| 131 |
"""Update all tabs and their close buttons"""
|
| 132 |
try:
|
| 133 |
+
for i in range(self.count()):
|
| 134 |
widget = self.widget(i)
|
| 135 |
if widget:
|
| 136 |
label = self.tabText(i)
|
| 137 |
tab_id = f"{label}_{id(widget)}"
|
| 138 |
|
| 139 |
+
# Create new close button if needed and if not Home or Startup tab
|
| 140 |
+
if (label not in ["Home", "Startup"] and
|
| 141 |
+
(tab_id not in self._tabs or not self._tabs[tab_id].get('close_button'))):
|
| 142 |
close_button = self._create_close_button(i, label)
|
| 143 |
self._tabs[tab_id] = {
|
| 144 |
'widget': widget,
|
|
|
|
| 146 |
'close_button': close_button
|
| 147 |
}
|
| 148 |
self.tabBar().setTabButton(i, QTabBar.ButtonPosition.RightSide, close_button)
|
| 149 |
+
elif label not in ["Home", "Startup"] and tab_id in self._tabs:
|
| 150 |
# Update existing close button's click connection
|
| 151 |
close_button = self._tabs[tab_id]['close_button']
|
| 152 |
close_button.clicked.disconnect()
|
|
@@ -0,0 +1,1227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene,
|
| 2 |
+
QGraphicsObject, QGraphicsSimpleTextItem, QApplication,
|
| 3 |
+
QLabel, QFrame, QGraphicsLineItem)
|
| 4 |
+
from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QPointF, QLineF, QSizeF, QTimer
|
| 5 |
+
from PyQt6.QtGui import QPainter, QPen, QBrush, QColor, QPainterPath, QFont, QPolygonF, QTransform, QKeySequence
|
| 6 |
+
|
| 7 |
+
class DNAFeatureViewer(QWidget): # Change to QWidget
|
| 8 |
+
"""Custom widget for displaying DNA features with sequence"""
|
| 9 |
+
sequence_selected = pyqtSignal(int, int) # Emit start and end positions when sequence is selected
|
| 10 |
+
|
| 11 |
+
def __init__(self, parent=None):
|
| 12 |
+
super().__init__(parent)
|
| 13 |
+
|
| 14 |
+
# Add logger
|
| 15 |
+
import logging
|
| 16 |
+
self.logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
# Create layout for this widget
|
| 19 |
+
self.layout = QVBoxLayout(self)
|
| 20 |
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
| 21 |
+
self.layout.setSpacing(0)
|
| 22 |
+
|
| 23 |
+
# Create graphics view with left alignment
|
| 24 |
+
self.view = QGraphicsView()
|
| 25 |
+
self.scene = QGraphicsScene(self)
|
| 26 |
+
self.view.setScene(self.scene)
|
| 27 |
+
self.view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
| 28 |
+
|
| 29 |
+
# Create components
|
| 30 |
+
self.sequence_viewer = SequenceViewer()
|
| 31 |
+
self.feature_viewer = FeatureViewer()
|
| 32 |
+
|
| 33 |
+
# Add components to scene
|
| 34 |
+
self.scene.addItem(self.sequence_viewer)
|
| 35 |
+
self.scene.addItem(self.feature_viewer)
|
| 36 |
+
|
| 37 |
+
# Connect signals from both viewers
|
| 38 |
+
self.sequence_viewer.sequence_selected.connect(self._on_sequence_selected)
|
| 39 |
+
self.sequence_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
|
| 40 |
+
self.feature_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
|
| 41 |
+
|
| 42 |
+
# Create status panel
|
| 43 |
+
self.status_panel = QLabel()
|
| 44 |
+
self.status_panel.setFrameStyle(QFrame.Shape.Panel | QFrame.Shadow.Sunken)
|
| 45 |
+
self.status_panel.setStyleSheet("""
|
| 46 |
+
QLabel {
|
| 47 |
+
background-color: #f0f0f0;
|
| 48 |
+
padding: 5px;
|
| 49 |
+
border-top: 1px solid #ccc;
|
| 50 |
+
min-height: 20px;
|
| 51 |
+
}
|
| 52 |
+
""")
|
| 53 |
+
|
| 54 |
+
# Add widgets to layout
|
| 55 |
+
self.layout.addWidget(self.view)
|
| 56 |
+
self.layout.addWidget(self.status_panel)
|
| 57 |
+
|
| 58 |
+
# Setup view
|
| 59 |
+
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
| 60 |
+
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
| 61 |
+
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
| 62 |
+
self.view.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
|
| 63 |
+
self.view.setMinimumHeight(200)
|
| 64 |
+
|
| 65 |
+
# Add resize event handling
|
| 66 |
+
self.view.viewport().installEventFilter(self)
|
| 67 |
+
|
| 68 |
+
# Add resize timer for debouncing
|
| 69 |
+
self.resize_timer = QTimer()
|
| 70 |
+
self.resize_timer.setSingleShot(True)
|
| 71 |
+
self.resize_timer.timeout.connect(self._delayed_resize)
|
| 72 |
+
self.cached_size = None
|
| 73 |
+
|
| 74 |
+
# Create ruler view with left alignment
|
| 75 |
+
self.ruler_view = QGraphicsView()
|
| 76 |
+
self.ruler_scene = QGraphicsScene(self)
|
| 77 |
+
self.ruler_view.setScene(self.ruler_scene)
|
| 78 |
+
self.ruler_view.setFixedHeight(25)
|
| 79 |
+
self.ruler_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
| 80 |
+
self.ruler_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
| 81 |
+
self.ruler_view.setViewportMargins(0, 0, 0, 0)
|
| 82 |
+
self.ruler_view.setFrameStyle(0)
|
| 83 |
+
self.ruler_view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
| 84 |
+
|
| 85 |
+
# Set opaque white background to hide sequence behind ruler
|
| 86 |
+
self.ruler_view.setBackgroundBrush(QBrush(Qt.GlobalColor.white))
|
| 87 |
+
self.ruler_view.setAutoFillBackground(True)
|
| 88 |
+
|
| 89 |
+
# Add ruler to layout before main view
|
| 90 |
+
self.layout.insertWidget(0, self.ruler_view)
|
| 91 |
+
|
| 92 |
+
# Connect horizontal scroll bars
|
| 93 |
+
self.view.horizontalScrollBar().valueChanged.connect(
|
| 94 |
+
self.ruler_view.horizontalScrollBar().setValue
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# Create initial ruler
|
| 98 |
+
self._create_ruler()
|
| 99 |
+
|
| 100 |
+
def eventFilter(self, obj, event):
|
| 101 |
+
"""Handle viewport resize events with debouncing"""
|
| 102 |
+
if obj == self.view.viewport() and event.type() == event.Type.Resize:
|
| 103 |
+
# Cache the new size
|
| 104 |
+
self.cached_size = event.size()
|
| 105 |
+
# Reset and start the timer
|
| 106 |
+
self.resize_timer.stop()
|
| 107 |
+
self.resize_timer.start(150) # 150ms delay
|
| 108 |
+
return super().eventFilter(obj, event)
|
| 109 |
+
|
| 110 |
+
def _delayed_resize(self):
|
| 111 |
+
"""Handle resize after debouncing period"""
|
| 112 |
+
if self.cached_size:
|
| 113 |
+
try:
|
| 114 |
+
self.view.setUpdatesEnabled(False)
|
| 115 |
+
self.sequence_viewer.setVisible(False)
|
| 116 |
+
self.feature_viewer.setVisible(False)
|
| 117 |
+
|
| 118 |
+
self._handle_resize(self.cached_size)
|
| 119 |
+
|
| 120 |
+
# Call reapply_highlights on sequence_viewer instead of self
|
| 121 |
+
self.sequence_viewer._reapply_highlights()
|
| 122 |
+
|
| 123 |
+
self.sequence_viewer.setVisible(True)
|
| 124 |
+
self.feature_viewer.setVisible(True)
|
| 125 |
+
self.view.setUpdatesEnabled(True)
|
| 126 |
+
|
| 127 |
+
# Force immediate update
|
| 128 |
+
self.view.viewport().update()
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
self.logger.error(f"Error in delayed resize: {str(e)}")
|
| 132 |
+
finally:
|
| 133 |
+
self.view.setUpdatesEnabled(True)
|
| 134 |
+
self.sequence_viewer.setVisible(True)
|
| 135 |
+
self.feature_viewer.setVisible(True)
|
| 136 |
+
|
| 137 |
+
def _handle_resize(self, new_size):
|
| 138 |
+
"""Handle viewport resize by adjusting sequence display"""
|
| 139 |
+
try:
|
| 140 |
+
viewport_width = max(1, new_size.width())
|
| 141 |
+
margin = 100 # Space for position numbers
|
| 142 |
+
|
| 143 |
+
# Calculate available width for bases
|
| 144 |
+
available_width = viewport_width - margin
|
| 145 |
+
base_width = self.sequence_viewer.base_width
|
| 146 |
+
|
| 147 |
+
# Calculate maximum number of bases that can fit
|
| 148 |
+
max_bases = (available_width // base_width)
|
| 149 |
+
|
| 150 |
+
# Round down to nearest multiple of 10
|
| 151 |
+
new_bases = (max_bases // 10) * 10
|
| 152 |
+
|
| 153 |
+
# Ensure minimum of 10 bases
|
| 154 |
+
new_bases = max(10, new_bases)
|
| 155 |
+
|
| 156 |
+
# Calculate total width needed
|
| 157 |
+
total_width = (new_bases * base_width) + margin
|
| 158 |
+
|
| 159 |
+
# Only update sequence if bases per line needs to change
|
| 160 |
+
if new_bases != self.sequence_viewer.bases_per_line:
|
| 161 |
+
self.logger.debug(
|
| 162 |
+
f"Resizing from {self.sequence_viewer.bases_per_line} to {new_bases} bases per line"
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# Batch update both viewers
|
| 166 |
+
self.view.setUpdatesEnabled(False)
|
| 167 |
+
self.ruler_view.setUpdatesEnabled(False)
|
| 168 |
+
|
| 169 |
+
# Update sequence viewer
|
| 170 |
+
self.sequence_viewer.bases_per_line = new_bases
|
| 171 |
+
self.sequence_viewer._create_nucleotide_items()
|
| 172 |
+
|
| 173 |
+
# Update feature viewer to match
|
| 174 |
+
if hasattr(self, 'feature_viewer'):
|
| 175 |
+
self.feature_viewer.bases_per_line = new_bases
|
| 176 |
+
self.feature_viewer.update()
|
| 177 |
+
|
| 178 |
+
# Calculate total height needed
|
| 179 |
+
sequence_length = len(self.sequence_viewer.sequence)
|
| 180 |
+
total_lines = (sequence_length + new_bases - 1) // new_bases
|
| 181 |
+
total_height = total_lines * self.sequence_viewer.line_spacing
|
| 182 |
+
|
| 183 |
+
# Set fixed scene rect with left alignment and full height
|
| 184 |
+
scene_rect = QRectF(0, 0, total_width, total_height)
|
| 185 |
+
self.scene.setSceneRect(scene_rect)
|
| 186 |
+
|
| 187 |
+
# Update ruler with exact viewport width
|
| 188 |
+
self._create_ruler()
|
| 189 |
+
self.ruler_view.setFixedWidth(viewport_width + 10)
|
| 190 |
+
# Set ruler scene rect to exactly match viewport width
|
| 191 |
+
self.ruler_scene.setSceneRect(0, 0, viewport_width + 10, 25)
|
| 192 |
+
|
| 193 |
+
# Keep scroll positions in sync
|
| 194 |
+
scroll_value = self.view.horizontalScrollBar().value()
|
| 195 |
+
self.ruler_view.horizontalScrollBar().setValue(scroll_value)
|
| 196 |
+
|
| 197 |
+
# Ensure view shows full content
|
| 198 |
+
self.view.setSceneRect(scene_rect)
|
| 199 |
+
|
| 200 |
+
self.view.setUpdatesEnabled(True)
|
| 201 |
+
self.ruler_view.setUpdatesEnabled(True)
|
| 202 |
+
|
| 203 |
+
# Force immediate update
|
| 204 |
+
self.view.viewport().update()
|
| 205 |
+
self.ruler_view.viewport().update()
|
| 206 |
+
else:
|
| 207 |
+
# Even if we don't resize, ensure proper alignment
|
| 208 |
+
self.ruler_view.setFixedWidth(viewport_width + 10)
|
| 209 |
+
# Update ruler scene rect to match viewport exactly
|
| 210 |
+
self.ruler_scene.setSceneRect(0, 0, viewport_width + 10, 25)
|
| 211 |
+
scroll_value = self.view.horizontalScrollBar().value()
|
| 212 |
+
self.ruler_view.horizontalScrollBar().setValue(scroll_value)
|
| 213 |
+
self.ruler_view.viewport().update()
|
| 214 |
+
|
| 215 |
+
except Exception as e:
|
| 216 |
+
self.logger.error(f"Error in _handle_resize: {str(e)}")
|
| 217 |
+
|
| 218 |
+
def set_data(self, sequence, features, start_pos=None):
|
| 219 |
+
"""Set data for both viewers"""
|
| 220 |
+
if start_pos is None:
|
| 221 |
+
start_pos = 0
|
| 222 |
+
|
| 223 |
+
# Update both components
|
| 224 |
+
self.sequence_viewer.set_data(sequence, start_pos)
|
| 225 |
+
self.feature_viewer.set_data(sequence, features, start_pos)
|
| 226 |
+
|
| 227 |
+
# Position feature viewer to overlap with sequence viewer
|
| 228 |
+
self.feature_viewer.setY(0)
|
| 229 |
+
|
| 230 |
+
# Update scene rect to encompass both viewers
|
| 231 |
+
combined_rect = self.sequence_viewer.boundingRect().united(self.feature_viewer.boundingRect())
|
| 232 |
+
self.scene.setSceneRect(combined_rect)
|
| 233 |
+
|
| 234 |
+
# Update status panel with initial sequence info
|
| 235 |
+
sequence_length = len(sequence)
|
| 236 |
+
self.status_panel.setText(f"Showing: {start_pos}...{start_pos + sequence_length} = {sequence_length} bp")
|
| 237 |
+
|
| 238 |
+
self.update()
|
| 239 |
+
|
| 240 |
+
def _on_sequence_selected(self, start_pos, end_pos):
|
| 241 |
+
"""Handle sequence selection"""
|
| 242 |
+
# Calculate selected length
|
| 243 |
+
selected_length = end_pos - start_pos + 1
|
| 244 |
+
|
| 245 |
+
# Update status panel with selection info
|
| 246 |
+
self.status_panel.setText(f"Selected: {start_pos}...{end_pos} = {selected_length} bp")
|
| 247 |
+
|
| 248 |
+
# Emit signal for other components
|
| 249 |
+
self.sequence_selected.emit(start_pos, end_pos)
|
| 250 |
+
|
| 251 |
+
def clear_selection(self):
|
| 252 |
+
"""Clear selection and reset status panel"""
|
| 253 |
+
if hasattr(self, 'sequence_viewer'):
|
| 254 |
+
sequence = self.sequence_viewer.sequence
|
| 255 |
+
start_pos = self.sequence_viewer.start_pos
|
| 256 |
+
sequence_length = len(sequence)
|
| 257 |
+
self.status_panel.setText(f"Showing: {start_pos}...{start_pos + sequence_length} = {sequence_length} bp")
|
| 258 |
+
|
| 259 |
+
def _on_cursor_position_changed(self, position):
|
| 260 |
+
"""Handle cursor position changes"""
|
| 261 |
+
if position >= 0:
|
| 262 |
+
self.status_panel.setText(f"Insertion Point: {position}")
|
| 263 |
+
else:
|
| 264 |
+
# Reset to showing current sequence range if cursor position is invalid
|
| 265 |
+
if hasattr(self, 'sequence_viewer'):
|
| 266 |
+
sequence = self.sequence_viewer.sequence
|
| 267 |
+
start_pos = self.sequence_viewer.start_pos
|
| 268 |
+
sequence_length = len(sequence)
|
| 269 |
+
self.status_panel.setText(f"Showing: {start_pos}...{start_pos + sequence_length} = {sequence_length} bp")
|
| 270 |
+
|
| 271 |
+
def _create_ruler(self):
|
| 272 |
+
"""Create ruler with position markers"""
|
| 273 |
+
try:
|
| 274 |
+
self.ruler_scene.clear()
|
| 275 |
+
|
| 276 |
+
# Get current bases per line
|
| 277 |
+
bases_per_line = self.sequence_viewer.bases_per_line
|
| 278 |
+
base_width = self.sequence_viewer.base_width
|
| 279 |
+
|
| 280 |
+
# Calculate total width including margin
|
| 281 |
+
total_width = bases_per_line * base_width + 100 # Match sequence viewer width
|
| 282 |
+
|
| 283 |
+
# Create horizontal blue line aligned with sequence
|
| 284 |
+
ruler_line = QGraphicsLineItem(0, 15, bases_per_line * base_width, 15)
|
| 285 |
+
ruler_line.setPen(QPen(QColor(0, 120, 215), 1))
|
| 286 |
+
self.ruler_scene.addItem(ruler_line)
|
| 287 |
+
|
| 288 |
+
# Add tick marks and numbers for every base
|
| 289 |
+
for i in range(0, bases_per_line):
|
| 290 |
+
x_pos = i * base_width + base_width/2 # Center tick marks between bases
|
| 291 |
+
|
| 292 |
+
# Use 1-based indexing for position calculation
|
| 293 |
+
pos_1_based = i + 1
|
| 294 |
+
|
| 295 |
+
# Determine tick height based on position
|
| 296 |
+
if pos_1_based % 10 == 0: # Major ticks (every 10)
|
| 297 |
+
tick_height = 10
|
| 298 |
+
tick_start = 10
|
| 299 |
+
# Add number
|
| 300 |
+
text = QGraphicsSimpleTextItem(str(pos_1_based))
|
| 301 |
+
text.setFont(QFont("Arial", 8))
|
| 302 |
+
text_width = text.boundingRect().width()
|
| 303 |
+
text.setPos(x_pos - text_width/2, 0) # Position above line
|
| 304 |
+
self.ruler_scene.addItem(text)
|
| 305 |
+
elif pos_1_based % 5 == 0: # Medium ticks (every 5)
|
| 306 |
+
tick_height = 7
|
| 307 |
+
tick_start = 11
|
| 308 |
+
else: # Small ticks (every 1)
|
| 309 |
+
tick_height = 4
|
| 310 |
+
tick_start = 13
|
| 311 |
+
|
| 312 |
+
# Create tick mark
|
| 313 |
+
tick = QGraphicsLineItem(x_pos, tick_start, x_pos, tick_start + tick_height)
|
| 314 |
+
tick.setPen(QPen(QColor(0, 120, 215), 1))
|
| 315 |
+
self.ruler_scene.addItem(tick)
|
| 316 |
+
|
| 317 |
+
# Set scene rect to exactly match sequence viewer width
|
| 318 |
+
self.ruler_scene.setSceneRect(0, 0, total_width, 25)
|
| 319 |
+
|
| 320 |
+
except Exception as e:
|
| 321 |
+
self.logger.error(f"Error creating ruler: {str(e)}")
|
| 322 |
+
|
| 323 |
+
class NucleotideItem(QGraphicsObject):
|
| 324 |
+
clicked = pyqtSignal(object)
|
| 325 |
+
|
| 326 |
+
def __init__(self, nucleotide, x, y, width, is_uppercase=False, is_complement=False, parent=None):
|
| 327 |
+
super().__init__(parent)
|
| 328 |
+
self.nucleotide = nucleotide
|
| 329 |
+
self.spacing = 0
|
| 330 |
+
self.base_width = width
|
| 331 |
+
self.rect = QRectF(0, 0, width, width * 2)
|
| 332 |
+
self.setPos(x, y)
|
| 333 |
+
self.is_uppercase = is_uppercase
|
| 334 |
+
self.is_complement = is_complement
|
| 335 |
+
self.is_highlighted = False
|
| 336 |
+
self.highlight_color = None
|
| 337 |
+
self.show_cursor = False
|
| 338 |
+
self.cursor_side = 'right'
|
| 339 |
+
self.setAcceptHoverEvents(True)
|
| 340 |
+
|
| 341 |
+
# Get logger from parent
|
| 342 |
+
sequence_viewer = self.parent()
|
| 343 |
+
if sequence_viewer and hasattr(sequence_viewer, 'logger'):
|
| 344 |
+
self.logger = sequence_viewer.logger
|
| 345 |
+
else:
|
| 346 |
+
import logging
|
| 347 |
+
self.logger = logging.getLogger(__name__)
|
| 348 |
+
|
| 349 |
+
def boundingRect(self):
|
| 350 |
+
return self.rect
|
| 351 |
+
|
| 352 |
+
def paint(self, painter, option, widget):
|
| 353 |
+
try:
|
| 354 |
+
# Draw highlight background if highlighted
|
| 355 |
+
if self.is_highlighted and self.highlight_color:
|
| 356 |
+
painter.fillRect(self.rect, self.highlight_color)
|
| 357 |
+
|
| 358 |
+
# Draw nucleotide
|
| 359 |
+
painter.setFont(QFont("Courier", 12))
|
| 360 |
+
if self.is_uppercase:
|
| 361 |
+
painter.setPen(Qt.GlobalColor.black)
|
| 362 |
+
else:
|
| 363 |
+
painter.setPen(QColor(100, 100, 100))
|
| 364 |
+
|
| 365 |
+
# Get complement nucleotide if needed
|
| 366 |
+
display_nucleotide = self.nucleotide
|
| 367 |
+
if self.is_complement:
|
| 368 |
+
complement_map = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G',
|
| 369 |
+
'a': 't', 't': 'a', 'g': 'c', 'c': 'g'}
|
| 370 |
+
display_nucleotide = complement_map.get(self.nucleotide, self.nucleotide)
|
| 371 |
+
|
| 372 |
+
# Draw text centered
|
| 373 |
+
painter.drawText(self.rect, Qt.AlignmentFlag.AlignCenter, display_nucleotide)
|
| 374 |
+
|
| 375 |
+
# Draw cursor if active
|
| 376 |
+
if self.show_cursor:
|
| 377 |
+
painter.setPen(QPen(Qt.GlobalColor.black, 1))
|
| 378 |
+
if self.cursor_side == 'right':
|
| 379 |
+
x = self.rect.right() - 1
|
| 380 |
+
else:
|
| 381 |
+
x = self.rect.left() + 1
|
| 382 |
+
cursor_height = self.rect.height()
|
| 383 |
+
painter.drawLine(QPointF(x, 0), QPointF(x, cursor_height))
|
| 384 |
+
|
| 385 |
+
except Exception as e:
|
| 386 |
+
self.logger.error(f"Error in paint: {str(e)}")
|
| 387 |
+
|
| 388 |
+
def mousePressEvent(self, event):
|
| 389 |
+
try:
|
| 390 |
+
if event.button() == Qt.MouseButton.LeftButton:
|
| 391 |
+
sequence_viewer = self.parent()
|
| 392 |
+
if sequence_viewer:
|
| 393 |
+
pos = sequence_viewer.get_nucleotide_position(self)
|
| 394 |
+
local_x = event.pos().x()
|
| 395 |
+
|
| 396 |
+
# Determine cursor position based on click location
|
| 397 |
+
is_right_side = local_x > self.rect.width() / 2
|
| 398 |
+
|
| 399 |
+
# Always show cursor
|
| 400 |
+
self.show_cursor = True
|
| 401 |
+
self.cursor_side = 'right' if is_right_side else 'left'
|
| 402 |
+
|
| 403 |
+
# Calculate cursor position
|
| 404 |
+
cursor_pos = pos + 1 if is_right_side else pos
|
| 405 |
+
|
| 406 |
+
self.logger.debug(f"""
|
| 407 |
+
Mouse click details:
|
| 408 |
+
- Nucleotide: {self.nucleotide}
|
| 409 |
+
- Click X: {local_x}
|
| 410 |
+
- Is right side: {is_right_side}
|
| 411 |
+
- Cursor position: {cursor_pos}
|
| 412 |
+
""")
|
| 413 |
+
|
| 414 |
+
# Start selection
|
| 415 |
+
sequence_viewer.selection_active = True
|
| 416 |
+
sequence_viewer.selection_start = cursor_pos
|
| 417 |
+
sequence_viewer.selection_end = cursor_pos
|
| 418 |
+
|
| 419 |
+
# Clear other cursors
|
| 420 |
+
for nuc in sequence_viewer.nucleotides:
|
| 421 |
+
if nuc != self:
|
| 422 |
+
nuc.show_cursor = False
|
| 423 |
+
nuc.update()
|
| 424 |
+
|
| 425 |
+
sequence_viewer.cursor_position_changed.emit(cursor_pos)
|
| 426 |
+
sequence_viewer._update_selection()
|
| 427 |
+
self.update()
|
| 428 |
+
|
| 429 |
+
except Exception as e:
|
| 430 |
+
self.logger.error(f"Error in mousePressEvent: {str(e)}")
|
| 431 |
+
|
| 432 |
+
event.accept()
|
| 433 |
+
|
| 434 |
+
def mouseMoveEvent(self, event):
|
| 435 |
+
sequence_viewer = self.parent()
|
| 436 |
+
if sequence_viewer and event.buttons() & Qt.MouseButton.LeftButton:
|
| 437 |
+
try:
|
| 438 |
+
pos = sequence_viewer.get_nucleotide_position(self)
|
| 439 |
+
local_x = event.pos().x()
|
| 440 |
+
|
| 441 |
+
# Calculate position relative to letter boundaries
|
| 442 |
+
text_x = (self.rect.width() - self.base_width) / 2
|
| 443 |
+
relative_x = local_x - text_x
|
| 444 |
+
|
| 445 |
+
# Force cursor to show
|
| 446 |
+
self.show_cursor = True
|
| 447 |
+
|
| 448 |
+
# Determine cursor position
|
| 449 |
+
if relative_x <= 0: # Before letter
|
| 450 |
+
self.cursor_side = 'left'
|
| 451 |
+
cursor_pos = pos
|
| 452 |
+
elif relative_x >= self.base_width: # After letter
|
| 453 |
+
self.cursor_side = 'right'
|
| 454 |
+
cursor_pos = pos + 1
|
| 455 |
+
else: # On letter
|
| 456 |
+
is_after = relative_x > self.base_width / 2
|
| 457 |
+
self.cursor_side = 'right' if is_after else 'left'
|
| 458 |
+
cursor_pos = pos + 1 if is_after else pos
|
| 459 |
+
|
| 460 |
+
# Update selection
|
| 461 |
+
sequence_viewer.selection_end = pos
|
| 462 |
+
|
| 463 |
+
# Clear other cursors
|
| 464 |
+
for nuc in sequence_viewer.nucleotides:
|
| 465 |
+
if nuc != self:
|
| 466 |
+
nuc.show_cursor = False
|
| 467 |
+
nuc.update()
|
| 468 |
+
|
| 469 |
+
sequence_viewer.cursor_position_changed.emit(cursor_pos)
|
| 470 |
+
sequence_viewer._update_selection()
|
| 471 |
+
self.update() # Force redraw
|
| 472 |
+
|
| 473 |
+
except Exception as e:
|
| 474 |
+
self.logger.error(f"Error in mouseMoveEvent: {str(e)}")
|
| 475 |
+
|
| 476 |
+
event.accept()
|
| 477 |
+
|
| 478 |
+
def hoverMoveEvent(self, event):
|
| 479 |
+
local_x = event.pos().x()
|
| 480 |
+
mid_point = self.rect.width() / 2
|
| 481 |
+
|
| 482 |
+
sequence_viewer = self.parent()
|
| 483 |
+
if sequence_viewer:
|
| 484 |
+
# Only show cursor if not selecting
|
| 485 |
+
if not sequence_viewer.selection_active:
|
| 486 |
+
self.show_cursor = True
|
| 487 |
+
self.cursor_side = 'right' if local_x >= mid_point else 'left'
|
| 488 |
+
|
| 489 |
+
# Clear cursor from other nucleotides
|
| 490 |
+
for nuc in sequence_viewer.nucleotides:
|
| 491 |
+
if nuc != self:
|
| 492 |
+
nuc.show_cursor = False
|
| 493 |
+
nuc.update()
|
| 494 |
+
|
| 495 |
+
pos = sequence_viewer.get_nucleotide_position(self)
|
| 496 |
+
cursor_pos = pos + 1 if self.cursor_side == 'right' else pos
|
| 497 |
+
sequence_viewer.cursor_position_changed.emit(cursor_pos)
|
| 498 |
+
self.update()
|
| 499 |
+
|
| 500 |
+
def hoverLeaveEvent(self, event):
|
| 501 |
+
sequence_viewer = self.parent()
|
| 502 |
+
if sequence_viewer and not sequence_viewer.selection_active:
|
| 503 |
+
self.show_cursor = False
|
| 504 |
+
self.update()
|
| 505 |
+
super().hoverLeaveEvent(event)
|
| 506 |
+
|
| 507 |
+
class SequenceViewer(QGraphicsObject):
|
| 508 |
+
sequence_selected = pyqtSignal(int, int)
|
| 509 |
+
cursor_position_changed = pyqtSignal(int) # New signal for cursor position
|
| 510 |
+
|
| 511 |
+
def __init__(self, parent=None):
|
| 512 |
+
super().__init__(parent)
|
| 513 |
+
self.sequence = ""
|
| 514 |
+
self.start_pos = 0
|
| 515 |
+
self.base_width = 15
|
| 516 |
+
self.bases_per_line = 70
|
| 517 |
+
self.line_height = 25
|
| 518 |
+
self.line_spacing = 80 # Increased spacing between gene lines
|
| 519 |
+
self.nucleotides = []
|
| 520 |
+
self.selection_start = None
|
| 521 |
+
self.selection_end = None
|
| 522 |
+
self.setAcceptHoverEvents(True)
|
| 523 |
+
|
| 524 |
+
# Add clipboard support
|
| 525 |
+
self.clipboard = QApplication.clipboard()
|
| 526 |
+
|
| 527 |
+
# Add tracking for drag start position
|
| 528 |
+
self.drag_start_pos = None
|
| 529 |
+
self.selection_active = False
|
| 530 |
+
|
| 531 |
+
# Enable mouse tracking
|
| 532 |
+
self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
|
| 533 |
+
self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemIsFocusable)
|
| 534 |
+
|
| 535 |
+
# Add logger
|
| 536 |
+
import logging
|
| 537 |
+
self.logger = logging.getLogger(__name__)
|
| 538 |
+
|
| 539 |
+
# Configure logging
|
| 540 |
+
handler = logging.StreamHandler()
|
| 541 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 542 |
+
handler.setFormatter(formatter)
|
| 543 |
+
self.logger.addHandler(handler)
|
| 544 |
+
self.logger.setLevel(logging.DEBUG)
|
| 545 |
+
|
| 546 |
+
# Add highlight state tracking
|
| 547 |
+
self.highlighted_regions = [] # List of (start, end, color, strand) tuples
|
| 548 |
+
|
| 549 |
+
def _get_text_width(self):
|
| 550 |
+
"""Calculate text width based on current bases per line"""
|
| 551 |
+
return self.base_width * self.bases_per_line
|
| 552 |
+
|
| 553 |
+
def set_data(self, sequence, start_pos):
|
| 554 |
+
self.sequence = sequence
|
| 555 |
+
self.start_pos = start_pos
|
| 556 |
+
self._create_nucleotide_items()
|
| 557 |
+
self.update()
|
| 558 |
+
|
| 559 |
+
def _create_nucleotide_items(self):
|
| 560 |
+
try:
|
| 561 |
+
# Get the view from the scene
|
| 562 |
+
view = None
|
| 563 |
+
if self.scene():
|
| 564 |
+
views = self.scene().views()
|
| 565 |
+
if views:
|
| 566 |
+
view = views[0]
|
| 567 |
+
view.setUpdatesEnabled(False) # Use view instead of scene
|
| 568 |
+
|
| 569 |
+
# Clear existing items
|
| 570 |
+
self.cleanup_graphics()
|
| 571 |
+
|
| 572 |
+
current_pos = 0
|
| 573 |
+
max_width = 0 # Track maximum line width
|
| 574 |
+
|
| 575 |
+
# Pre-calculate total lines
|
| 576 |
+
total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
|
| 577 |
+
|
| 578 |
+
# Pre-allocate lists for better performance
|
| 579 |
+
self.plot_lines = []
|
| 580 |
+
self.tick_lines = []
|
| 581 |
+
self.nucleotides = []
|
| 582 |
+
|
| 583 |
+
# Track nucleotides by strand and line
|
| 584 |
+
self.nucleotide_map = {
|
| 585 |
+
'+': [], # List of lists for positive strand nucleotides by line
|
| 586 |
+
'-': [] # List of lists for negative strand nucleotides by line
|
| 587 |
+
}
|
| 588 |
+
current_line = 0
|
| 589 |
+
self.nucleotide_map['+'].append([]) # Initialize first line
|
| 590 |
+
self.nucleotide_map['-'].append([])
|
| 591 |
+
|
| 592 |
+
# Create nucleotides and setup display
|
| 593 |
+
self._create_display()
|
| 594 |
+
|
| 595 |
+
# Reapply highlights after creating nucleotides
|
| 596 |
+
self._reapply_highlights()
|
| 597 |
+
|
| 598 |
+
except Exception as e:
|
| 599 |
+
self.logger.error(f"Error in _create_nucleotide_items: {str(e)}")
|
| 600 |
+
# Make sure we re-enable updates even if there's an error
|
| 601 |
+
if view:
|
| 602 |
+
view.setUpdatesEnabled(True)
|
| 603 |
+
|
| 604 |
+
def _create_display(self):
|
| 605 |
+
"""Create the nucleotide display"""
|
| 606 |
+
try:
|
| 607 |
+
current_pos = 0
|
| 608 |
+
max_width = 0 # Add max_width definition here
|
| 609 |
+
|
| 610 |
+
while current_pos < len(self.sequence):
|
| 611 |
+
# Calculate exact number of bases for this line
|
| 612 |
+
remaining_bases = len(self.sequence) - current_pos
|
| 613 |
+
bases_this_line = min(self.bases_per_line, remaining_bases)
|
| 614 |
+
line_text = self.sequence[current_pos:current_pos + bases_this_line]
|
| 615 |
+
|
| 616 |
+
line_num = current_pos // self.bases_per_line
|
| 617 |
+
y_pos = line_num * self.line_spacing
|
| 618 |
+
|
| 619 |
+
# Calculate width for position numbers
|
| 620 |
+
max_width = max(max_width, self.bases_per_line * self.base_width)
|
| 621 |
+
|
| 622 |
+
# Create positive strand nucleotides
|
| 623 |
+
line_nucleotides_pos = []
|
| 624 |
+
for i, nucleotide in enumerate(line_text):
|
| 625 |
+
x_pos = i * self.base_width
|
| 626 |
+
nuc_item = NucleotideItem(
|
| 627 |
+
nucleotide=nucleotide,
|
| 628 |
+
x=x_pos,
|
| 629 |
+
y=y_pos + self.line_height * -0.3,
|
| 630 |
+
width=self.base_width,
|
| 631 |
+
is_uppercase=nucleotide.isupper(),
|
| 632 |
+
parent=self
|
| 633 |
+
)
|
| 634 |
+
self.nucleotides.append(nuc_item)
|
| 635 |
+
line_nucleotides_pos.append(nuc_item)
|
| 636 |
+
|
| 637 |
+
# Create complement strand nucleotides
|
| 638 |
+
line_nucleotides_neg = []
|
| 639 |
+
for i, nucleotide in enumerate(line_text):
|
| 640 |
+
x_pos = i * self.base_width
|
| 641 |
+
nuc_item = NucleotideItem(
|
| 642 |
+
nucleotide=nucleotide,
|
| 643 |
+
x=x_pos,
|
| 644 |
+
y=y_pos + self.line_height * 1.1,
|
| 645 |
+
width=self.base_width,
|
| 646 |
+
is_uppercase=nucleotide.isupper(),
|
| 647 |
+
is_complement=True,
|
| 648 |
+
parent=self
|
| 649 |
+
)
|
| 650 |
+
self.nucleotides.append(nuc_item)
|
| 651 |
+
line_nucleotides_neg.append(nuc_item)
|
| 652 |
+
|
| 653 |
+
# Store nucleotides by strand and line
|
| 654 |
+
if line_num >= len(self.nucleotide_map['+']):
|
| 655 |
+
self.nucleotide_map['+'].append([])
|
| 656 |
+
self.nucleotide_map['-'].append([])
|
| 657 |
+
self.nucleotide_map['+'][line_num].extend(line_nucleotides_pos)
|
| 658 |
+
self.nucleotide_map['-'][line_num].extend(line_nucleotides_neg)
|
| 659 |
+
|
| 660 |
+
# Draw plot line matching exactly the sequence width for this line
|
| 661 |
+
plot_y = y_pos + self.line_height
|
| 662 |
+
plot_line = QGraphicsLineItem(0, plot_y,
|
| 663 |
+
bases_this_line * self.base_width, plot_y, self)
|
| 664 |
+
plot_line.setPen(QPen(Qt.GlobalColor.black, 1))
|
| 665 |
+
self.plot_lines.append(plot_line)
|
| 666 |
+
|
| 667 |
+
# Draw tick marks only for actual bases in this line
|
| 668 |
+
for i in range(bases_this_line):
|
| 669 |
+
x_pos = i * self.base_width
|
| 670 |
+
|
| 671 |
+
# Convert to 1-based index for position calculation
|
| 672 |
+
pos_1_based = current_pos + i + 1 # Add 1 for 1-based indexing
|
| 673 |
+
|
| 674 |
+
# Determine tick height based on position
|
| 675 |
+
if i == 0 and current_pos == 0: # First base
|
| 676 |
+
tick_height = 12 # Longest tick for start
|
| 677 |
+
elif i == bases_this_line - 1 and current_pos + bases_this_line == len(self.sequence): # Last base
|
| 678 |
+
tick_height = 12 # Longest tick for end
|
| 679 |
+
elif pos_1_based % 10 == 0: # Major ticks (every 10)
|
| 680 |
+
tick_height = 10
|
| 681 |
+
elif pos_1_based % 5 == 0: # Medium ticks (every 5)
|
| 682 |
+
tick_height = 8
|
| 683 |
+
else: # Regular ticks
|
| 684 |
+
tick_height = 5
|
| 685 |
+
|
| 686 |
+
tick_line = QGraphicsLineItem(
|
| 687 |
+
x_pos + self.base_width/2,
|
| 688 |
+
plot_y - tick_height/2,
|
| 689 |
+
x_pos + self.base_width/2,
|
| 690 |
+
plot_y + tick_height/2,
|
| 691 |
+
self
|
| 692 |
+
)
|
| 693 |
+
self.tick_lines.append(tick_line)
|
| 694 |
+
|
| 695 |
+
# Add position number aligned with the plot line - remove the +1
|
| 696 |
+
end_pos = str(self.start_pos + current_pos + bases_this_line) # Removed +1
|
| 697 |
+
pos_item = QGraphicsSimpleTextItem(end_pos, self)
|
| 698 |
+
pos_item.setFont(QFont("Courier", 12))
|
| 699 |
+
|
| 700 |
+
# Calculate position for consistent alignment
|
| 701 |
+
text_width = pos_item.boundingRect().width()
|
| 702 |
+
pos_x = max_width + 10 # Fixed position based on maximum width
|
| 703 |
+
pos_y = plot_y - pos_item.boundingRect().height()/2
|
| 704 |
+
pos_item.setPos(pos_x, pos_y)
|
| 705 |
+
|
| 706 |
+
current_pos += bases_this_line
|
| 707 |
+
|
| 708 |
+
# Get parent view if it exists
|
| 709 |
+
view = self.scene().views()[0] if self.scene() and self.scene().views() else None
|
| 710 |
+
if view:
|
| 711 |
+
view.setUpdatesEnabled(True) # Re-enable updates if we have a view
|
| 712 |
+
self.update()
|
| 713 |
+
|
| 714 |
+
except Exception as e:
|
| 715 |
+
self.logger.error(f"Error in _create_display: {str(e)}")
|
| 716 |
+
|
| 717 |
+
def _reapply_highlights(self):
|
| 718 |
+
"""Reapply stored highlights after recreating nucleotides"""
|
| 719 |
+
try:
|
| 720 |
+
# Clear existing highlights from nucleotides
|
| 721 |
+
for nuc in self.nucleotides:
|
| 722 |
+
nuc.is_highlighted = False
|
| 723 |
+
nuc.highlight_color = None
|
| 724 |
+
|
| 725 |
+
# Reapply each stored highlight
|
| 726 |
+
for start_pos, end_pos, color, strand in self.highlighted_regions:
|
| 727 |
+
# Calculate which lines contain the sequence
|
| 728 |
+
start_line = start_pos // self.bases_per_line
|
| 729 |
+
end_line = end_pos // self.bases_per_line
|
| 730 |
+
|
| 731 |
+
# Calculate positions within lines
|
| 732 |
+
start_pos_in_line = start_pos % self.bases_per_line
|
| 733 |
+
end_pos_in_line = end_pos % self.bases_per_line
|
| 734 |
+
|
| 735 |
+
# Get the correct strand's nucleotide map
|
| 736 |
+
strand_map = self.nucleotide_map[strand]
|
| 737 |
+
|
| 738 |
+
# Handle multi-line sequences
|
| 739 |
+
for line_num in range(start_line, end_line + 1):
|
| 740 |
+
if line_num >= len(strand_map):
|
| 741 |
+
continue
|
| 742 |
+
|
| 743 |
+
# Calculate start and end positions for this line
|
| 744 |
+
if line_num == start_line:
|
| 745 |
+
line_start = start_pos_in_line
|
| 746 |
+
else:
|
| 747 |
+
line_start = 0
|
| 748 |
+
|
| 749 |
+
if line_num == end_line:
|
| 750 |
+
line_end = end_pos_in_line
|
| 751 |
+
else:
|
| 752 |
+
line_end = self.bases_per_line - 1
|
| 753 |
+
|
| 754 |
+
# Get nucleotides for this line segment
|
| 755 |
+
line_nucleotides = strand_map[line_num]
|
| 756 |
+
|
| 757 |
+
# Calculate the range of nucleotides to highlight
|
| 758 |
+
start_idx = min(line_start, len(line_nucleotides))
|
| 759 |
+
end_idx = min(line_end + 1, len(line_nucleotides))
|
| 760 |
+
|
| 761 |
+
# Highlight the nucleotides
|
| 762 |
+
for i in range(start_idx, end_idx):
|
| 763 |
+
nuc = line_nucleotides[i]
|
| 764 |
+
nuc.is_highlighted = True
|
| 765 |
+
nuc.highlight_color = color
|
| 766 |
+
nuc.update()
|
| 767 |
+
|
| 768 |
+
except Exception as e:
|
| 769 |
+
self.logger.error(f"Error in _reapply_highlights: {str(e)}")
|
| 770 |
+
|
| 771 |
+
def _update_selection(self):
|
| 772 |
+
"""Update the visual selection"""
|
| 773 |
+
try:
|
| 774 |
+
if self.selection_start is None:
|
| 775 |
+
self.logger.debug("No selection start point")
|
| 776 |
+
return
|
| 777 |
+
|
| 778 |
+
start_idx = min(self.selection_start, self.selection_end or self.selection_start)
|
| 779 |
+
end_idx = max(self.selection_start, self.selection_end or self.selection_start)
|
| 780 |
+
|
| 781 |
+
self.logger.debug(f"Updating selection: start={start_idx}, end={end_idx}")
|
| 782 |
+
|
| 783 |
+
# Update highlighting for all nucleotides
|
| 784 |
+
for i, nuc in enumerate(self.nucleotides):
|
| 785 |
+
if start_idx <= i <= end_idx:
|
| 786 |
+
nuc.is_highlighted = True
|
| 787 |
+
nuc.highlight_color = QColor(200, 200, 255, 100)
|
| 788 |
+
self.logger.debug(f"Highlighting nucleotide at position {i}: {nuc.nucleotide}")
|
| 789 |
+
else:
|
| 790 |
+
nuc.is_highlighted = False
|
| 791 |
+
nuc.highlight_color = None
|
| 792 |
+
nuc.update()
|
| 793 |
+
|
| 794 |
+
# Emit selection signal
|
| 795 |
+
if self.selection_active:
|
| 796 |
+
self.logger.debug(f"Emitting selection signal: {self.start_pos + start_idx} to {self.start_pos + end_idx}")
|
| 797 |
+
self.sequence_selected.emit(
|
| 798 |
+
self.start_pos + start_idx,
|
| 799 |
+
self.start_pos + end_idx
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
except Exception as e:
|
| 803 |
+
self.logger.error(f"Error in _update_selection: {str(e)}")
|
| 804 |
+
|
| 805 |
+
def mousePressEvent(self, event):
|
| 806 |
+
"""Handle mouse press for selection start"""
|
| 807 |
+
try:
|
| 808 |
+
# Convert scene position to local coordinates
|
| 809 |
+
local_pos = self.mapFromScene(event.scenePos())
|
| 810 |
+
|
| 811 |
+
# Calculate base position more precisely
|
| 812 |
+
x_pos = local_pos.x()
|
| 813 |
+
line_number = int(local_pos.y() // (self.line_height * 2))
|
| 814 |
+
|
| 815 |
+
# Calculate which letter space was clicked
|
| 816 |
+
exact_position = x_pos / self.base_width
|
| 817 |
+
base_position = int(exact_position)
|
| 818 |
+
|
| 819 |
+
# Calculate if click was in left or right half of the letter space
|
| 820 |
+
fraction = exact_position - base_position
|
| 821 |
+
is_right_side = fraction > 0.5
|
| 822 |
+
|
| 823 |
+
# Adjust base_position based on where exactly the click occurred
|
| 824 |
+
if is_right_side:
|
| 825 |
+
cursor_index = base_position
|
| 826 |
+
cursor_side = 'right'
|
| 827 |
+
else:
|
| 828 |
+
cursor_index = max(0, base_position - 1)
|
| 829 |
+
cursor_side = 'right' if base_position == 0 else 'left'
|
| 830 |
+
|
| 831 |
+
# Calculate final index
|
| 832 |
+
index = line_number * self.bases_per_line + cursor_index
|
| 833 |
+
index = max(0, min(index, len(self.nucleotides) - 1))
|
| 834 |
+
|
| 835 |
+
if 0 <= index < len(self.nucleotides):
|
| 836 |
+
# Start selection
|
| 837 |
+
self.selection_active = True
|
| 838 |
+
self.selection_start = index
|
| 839 |
+
self.selection_end = index
|
| 840 |
+
|
| 841 |
+
# Update cursor position
|
| 842 |
+
nuc = self.nucleotides[index]
|
| 843 |
+
nuc.show_cursor = True
|
| 844 |
+
nuc.cursor_side = cursor_side
|
| 845 |
+
|
| 846 |
+
# Clear other cursors
|
| 847 |
+
for i, other_nuc in enumerate(self.nucleotides):
|
| 848 |
+
if i != index:
|
| 849 |
+
other_nuc.show_cursor = False
|
| 850 |
+
other_nuc.update()
|
| 851 |
+
|
| 852 |
+
# Update selection and cursor
|
| 853 |
+
cursor_pos = index + 1 if cursor_side == 'right' else index
|
| 854 |
+
self.cursor_position_changed.emit(self.start_pos + cursor_pos)
|
| 855 |
+
self._update_selection()
|
| 856 |
+
nuc.update()
|
| 857 |
+
|
| 858 |
+
except Exception as e:
|
| 859 |
+
print(f"Error in mousePressEvent: {str(e)}")
|
| 860 |
+
|
| 861 |
+
event.accept()
|
| 862 |
+
|
| 863 |
+
def mouseMoveEvent(self, event):
|
| 864 |
+
"""Handle mouse move for selection update"""
|
| 865 |
+
if not self.selection_active:
|
| 866 |
+
return
|
| 867 |
+
|
| 868 |
+
try:
|
| 869 |
+
# Convert scene position to local coordinates
|
| 870 |
+
local_pos = self.mapFromScene(event.scenePos())
|
| 871 |
+
|
| 872 |
+
# Calculate base position more precisely
|
| 873 |
+
x_pos = local_pos.x()
|
| 874 |
+
line_number = int(local_pos.y() // (self.line_height * 2))
|
| 875 |
+
|
| 876 |
+
# Calculate which letter space was clicked
|
| 877 |
+
exact_position = x_pos / self.base_width
|
| 878 |
+
base_position = int(exact_position)
|
| 879 |
+
|
| 880 |
+
# Calculate if mouse is in left or right half of the letter space
|
| 881 |
+
fraction = exact_position - base_position
|
| 882 |
+
is_right_side = fraction > 0.5
|
| 883 |
+
|
| 884 |
+
# Adjust base_position based on mouse position
|
| 885 |
+
if is_right_side:
|
| 886 |
+
cursor_index = base_position
|
| 887 |
+
cursor_side = 'right'
|
| 888 |
+
else:
|
| 889 |
+
cursor_index = max(0, base_position - 1)
|
| 890 |
+
cursor_side = 'right' if base_position == 0 else 'left'
|
| 891 |
+
|
| 892 |
+
# Calculate final index
|
| 893 |
+
index = line_number * self.bases_per_line + cursor_index
|
| 894 |
+
index = max(0, min(index, len(self.nucleotides) - 1))
|
| 895 |
+
|
| 896 |
+
if 0 <= index < len(self.nucleotides):
|
| 897 |
+
# Update selection end point
|
| 898 |
+
self.selection_end = index
|
| 899 |
+
|
| 900 |
+
# Update cursor position
|
| 901 |
+
nuc = self.nucleotides[index]
|
| 902 |
+
nuc.show_cursor = True
|
| 903 |
+
nuc.cursor_side = cursor_side
|
| 904 |
+
|
| 905 |
+
# Clear other cursors
|
| 906 |
+
for i, other_nuc in enumerate(self.nucleotides):
|
| 907 |
+
if i != index:
|
| 908 |
+
other_nuc.show_cursor = False
|
| 909 |
+
other_nuc.update()
|
| 910 |
+
|
| 911 |
+
# Update selection and cursor
|
| 912 |
+
cursor_pos = index + 1 if cursor_side == 'right' else index
|
| 913 |
+
self.cursor_position_changed.emit(self.start_pos + cursor_pos)
|
| 914 |
+
self._update_selection()
|
| 915 |
+
nuc.update()
|
| 916 |
+
|
| 917 |
+
except Exception as e:
|
| 918 |
+
print(f"Error in mouseMoveEvent: {str(e)}")
|
| 919 |
+
|
| 920 |
+
event.accept()
|
| 921 |
+
|
| 922 |
+
def mouseReleaseEvent(self, event):
|
| 923 |
+
"""Handle mouse release"""
|
| 924 |
+
if self.selection_active:
|
| 925 |
+
if self.selection_start is not None and self.selection_end is not None:
|
| 926 |
+
start_pos = min(self.selection_start, self.selection_end)
|
| 927 |
+
end_pos = max(self.selection_start, self.selection_end)
|
| 928 |
+
|
| 929 |
+
# Get selected sequence
|
| 930 |
+
selected_sequence = self._get_selected_sequence(start_pos, end_pos)
|
| 931 |
+
|
| 932 |
+
# Copy to clipboard
|
| 933 |
+
self.clipboard.setText(selected_sequence)
|
| 934 |
+
|
| 935 |
+
# Emit selection signal
|
| 936 |
+
self.sequence_selected.emit(
|
| 937 |
+
self.start_pos + start_pos,
|
| 938 |
+
self.start_pos + end_pos
|
| 939 |
+
)
|
| 940 |
+
else:
|
| 941 |
+
# If no selection was made, keep showing the insertion point
|
| 942 |
+
if hasattr(self, 'drag_start_pos') and self.drag_start_pos is not None:
|
| 943 |
+
self.cursor_position_changed.emit(self.start_pos + self.drag_start_pos)
|
| 944 |
+
|
| 945 |
+
event.accept()
|
| 946 |
+
|
| 947 |
+
def _find_closest_nucleotide(self, pos):
|
| 948 |
+
"""Find the closest nucleotide to the given position"""
|
| 949 |
+
closest_item = None
|
| 950 |
+
min_distance = float('inf')
|
| 951 |
+
|
| 952 |
+
for nuc in self.nucleotides:
|
| 953 |
+
nuc_pos = nuc.scenePos()
|
| 954 |
+
distance = (pos.x() - nuc_pos.x()) ** 2 + (pos.y() - nuc_pos.y()) ** 2
|
| 955 |
+
|
| 956 |
+
if distance < min_distance:
|
| 957 |
+
min_distance = distance
|
| 958 |
+
closest_item = nuc
|
| 959 |
+
|
| 960 |
+
return closest_item
|
| 961 |
+
|
| 962 |
+
def _get_selected_sequence(self, start_pos, end_pos):
|
| 963 |
+
"""Get the sequence of selected nucleotides"""
|
| 964 |
+
selected_nucs = []
|
| 965 |
+
for i in range(start_pos, end_pos + 1):
|
| 966 |
+
if i < len(self.nucleotides):
|
| 967 |
+
selected_nucs.append(self.nucleotides[i].nucleotide)
|
| 968 |
+
return ''.join(selected_nucs)
|
| 969 |
+
|
| 970 |
+
def keyPressEvent(self, event):
|
| 971 |
+
"""Handle keyboard shortcuts"""
|
| 972 |
+
if event.matches(QKeySequence.StandardKey.Copy):
|
| 973 |
+
if self.selection_start is not None and self.selection_end is not None:
|
| 974 |
+
start_pos = min(self.selection_start, self.selection_end)
|
| 975 |
+
end_pos = max(self.selection_start, self.selection_end)
|
| 976 |
+
selected_sequence = self._get_selected_sequence(start_pos, end_pos)
|
| 977 |
+
self.clipboard.setText(selected_sequence)
|
| 978 |
+
super().keyPressEvent(event)
|
| 979 |
+
|
| 980 |
+
def highlight_sequence(self, start_pos, end_pos, color, strand='+'):
|
| 981 |
+
"""Highlight sequence with proper strand handling"""
|
| 982 |
+
try:
|
| 983 |
+
# Store highlight information
|
| 984 |
+
self.highlighted_regions.append((start_pos, end_pos, color, strand))
|
| 985 |
+
|
| 986 |
+
# Calculate which lines contain the sequence
|
| 987 |
+
start_line = start_pos // self.bases_per_line
|
| 988 |
+
end_line = end_pos // self.bases_per_line
|
| 989 |
+
|
| 990 |
+
# Calculate positions within lines
|
| 991 |
+
start_pos_in_line = start_pos % self.bases_per_line
|
| 992 |
+
end_pos_in_line = end_pos % self.bases_per_line
|
| 993 |
+
|
| 994 |
+
# Get the correct strand's nucleotide map
|
| 995 |
+
strand_map = self.nucleotide_map[strand]
|
| 996 |
+
|
| 997 |
+
# Handle multi-line sequences
|
| 998 |
+
for line_num in range(start_line, end_line + 1):
|
| 999 |
+
if line_num >= len(strand_map):
|
| 1000 |
+
continue
|
| 1001 |
+
|
| 1002 |
+
# Calculate start and end positions for this line
|
| 1003 |
+
if line_num == start_line:
|
| 1004 |
+
line_start = start_pos_in_line
|
| 1005 |
+
else:
|
| 1006 |
+
line_start = 0
|
| 1007 |
+
|
| 1008 |
+
if line_num == end_line:
|
| 1009 |
+
line_end = end_pos_in_line
|
| 1010 |
+
else:
|
| 1011 |
+
line_end = self.bases_per_line - 1
|
| 1012 |
+
|
| 1013 |
+
# Get nucleotides for this line segment
|
| 1014 |
+
line_nucleotides = strand_map[line_num]
|
| 1015 |
+
|
| 1016 |
+
# Calculate the range of nucleotides to highlight
|
| 1017 |
+
start_idx = min(line_start, len(line_nucleotides))
|
| 1018 |
+
end_idx = min(line_end + 1, len(line_nucleotides))
|
| 1019 |
+
|
| 1020 |
+
# Highlight the nucleotides
|
| 1021 |
+
for i in range(start_idx, end_idx):
|
| 1022 |
+
nuc = line_nucleotides[i]
|
| 1023 |
+
nuc.is_highlighted = True
|
| 1024 |
+
nuc.highlight_color = color
|
| 1025 |
+
nuc.update()
|
| 1026 |
+
|
| 1027 |
+
self.logger.debug(
|
| 1028 |
+
f"Highlighted nucleotides on line {line_num} "
|
| 1029 |
+
f"for strand {strand} from {start_idx} to {end_idx}"
|
| 1030 |
+
)
|
| 1031 |
+
|
| 1032 |
+
except Exception as e:
|
| 1033 |
+
self.logger.error(f"Error in highlight_sequence: {str(e)}")
|
| 1034 |
+
self.logger.error(f"Start pos: {start_pos}, End pos: {end_pos}, Strand: {strand}")
|
| 1035 |
+
|
| 1036 |
+
def clear_highlights(self):
|
| 1037 |
+
"""Clear all highlights"""
|
| 1038 |
+
self.highlighted_regions.clear()
|
| 1039 |
+
for nuc in self.nucleotides:
|
| 1040 |
+
nuc.is_highlighted = False
|
| 1041 |
+
nuc.highlight_color = None
|
| 1042 |
+
nuc.update()
|
| 1043 |
+
|
| 1044 |
+
def boundingRect(self):
|
| 1045 |
+
if not self.sequence:
|
| 1046 |
+
return QRectF()
|
| 1047 |
+
|
| 1048 |
+
# Calculate exact width based on actual sequence in last line
|
| 1049 |
+
last_line_length = len(self.sequence) % self.bases_per_line
|
| 1050 |
+
if last_line_length == 0 and len(self.sequence) > 0:
|
| 1051 |
+
last_line_length = self.bases_per_line
|
| 1052 |
+
|
| 1053 |
+
width = self.base_width * last_line_length + 100 # Add space for position numbers
|
| 1054 |
+
|
| 1055 |
+
# Calculate height using line spacing
|
| 1056 |
+
total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
|
| 1057 |
+
height = total_lines * self.line_spacing
|
| 1058 |
+
|
| 1059 |
+
return QRectF(0, 0, width, height)
|
| 1060 |
+
|
| 1061 |
+
def get_nucleotide_position(self, nucleotide):
|
| 1062 |
+
"""Get the position of a nucleotide in the sequence"""
|
| 1063 |
+
try:
|
| 1064 |
+
idx = self.nucleotides.index(nucleotide)
|
| 1065 |
+
return self.start_pos + idx
|
| 1066 |
+
except ValueError:
|
| 1067 |
+
return -1
|
| 1068 |
+
|
| 1069 |
+
def cleanup_graphics(self):
|
| 1070 |
+
"""Clean up all graphics items"""
|
| 1071 |
+
if hasattr(self, 'plot_lines'):
|
| 1072 |
+
for line in self.plot_lines:
|
| 1073 |
+
if line in self.scene().items():
|
| 1074 |
+
self.scene().removeItem(line)
|
| 1075 |
+
self.plot_lines.clear()
|
| 1076 |
+
|
| 1077 |
+
if hasattr(self, 'tick_lines'):
|
| 1078 |
+
for line in self.tick_lines:
|
| 1079 |
+
if line in self.scene().items():
|
| 1080 |
+
self.scene().removeItem(line)
|
| 1081 |
+
self.tick_lines.clear()
|
| 1082 |
+
|
| 1083 |
+
for nuc in self.nucleotides:
|
| 1084 |
+
if nuc in self.scene().items():
|
| 1085 |
+
self.scene().removeItem(nuc)
|
| 1086 |
+
self.nucleotides.clear()
|
| 1087 |
+
|
| 1088 |
+
for item in self.scene().items():
|
| 1089 |
+
if isinstance(item, QGraphicsSimpleTextItem):
|
| 1090 |
+
self.scene().removeItem(item)
|
| 1091 |
+
|
| 1092 |
+
class FeatureViewer(QGraphicsObject):
|
| 1093 |
+
cursor_position_changed = pyqtSignal(int) # Add signal for cursor position
|
| 1094 |
+
|
| 1095 |
+
def __init__(self, parent=None):
|
| 1096 |
+
super().__init__(parent)
|
| 1097 |
+
self.sequence = ""
|
| 1098 |
+
self.features = []
|
| 1099 |
+
self.start_pos = 0
|
| 1100 |
+
self.base_width = 15
|
| 1101 |
+
self.bases_per_line = 70
|
| 1102 |
+
self.feature_height = 20
|
| 1103 |
+
self.line_height = 25
|
| 1104 |
+
self.feature_spacing = 2 # Reduce spacing between features and strands
|
| 1105 |
+
self.setAcceptHoverEvents(True) # Enable hover events
|
| 1106 |
+
|
| 1107 |
+
def set_data(self, sequence, features, start_pos):
|
| 1108 |
+
"""Updated to accept sequence parameter"""
|
| 1109 |
+
self.sequence = sequence
|
| 1110 |
+
self.features = sorted(features, key=lambda x: x['start'])
|
| 1111 |
+
self.start_pos = start_pos
|
| 1112 |
+
self.update()
|
| 1113 |
+
|
| 1114 |
+
def paint(self, painter, option, widget):
|
| 1115 |
+
if not self.features or not self.sequence:
|
| 1116 |
+
return
|
| 1117 |
+
|
| 1118 |
+
# Process each line of sequence
|
| 1119 |
+
current_pos = 0
|
| 1120 |
+
while current_pos < len(self.sequence):
|
| 1121 |
+
line_text = self.sequence[current_pos:current_pos + self.bases_per_line]
|
| 1122 |
+
line_num = current_pos // self.bases_per_line
|
| 1123 |
+
|
| 1124 |
+
# Calculate y position to be directly below negative strand
|
| 1125 |
+
y_pos = line_num * self.line_height * 2
|
| 1126 |
+
feature_y = y_pos + self.line_height * 2 # Position directly below negative strand
|
| 1127 |
+
|
| 1128 |
+
# Calculate sequence width for this line
|
| 1129 |
+
sequence_width = len(line_text) * self.base_width
|
| 1130 |
+
|
| 1131 |
+
# Draw features
|
| 1132 |
+
for feature in self.features:
|
| 1133 |
+
try:
|
| 1134 |
+
# Calculate relative positions within current line
|
| 1135 |
+
feature_start = feature['start'] - current_pos
|
| 1136 |
+
feature_end = feature['end'] - current_pos
|
| 1137 |
+
|
| 1138 |
+
# Skip if feature is not in current line
|
| 1139 |
+
if feature_end < 0 or feature_start >= self.bases_per_line:
|
| 1140 |
+
continue
|
| 1141 |
+
|
| 1142 |
+
# Clip to line boundaries
|
| 1143 |
+
feature_start = max(0, feature_start)
|
| 1144 |
+
feature_end = min(self.bases_per_line, feature_end)
|
| 1145 |
+
|
| 1146 |
+
# Calculate pixel positions
|
| 1147 |
+
x_start = feature_start * self.base_width
|
| 1148 |
+
x_end = feature_end * self.base_width
|
| 1149 |
+
|
| 1150 |
+
# Create rectangle for feature
|
| 1151 |
+
feature_rect = QRectF(
|
| 1152 |
+
x_start,
|
| 1153 |
+
feature_y,
|
| 1154 |
+
x_end - x_start,
|
| 1155 |
+
self.feature_height
|
| 1156 |
+
)
|
| 1157 |
+
|
| 1158 |
+
# Draw orange rectangle
|
| 1159 |
+
painter.setBrush(QColor(255, 140, 0))
|
| 1160 |
+
painter.setPen(Qt.PenStyle.NoPen)
|
| 1161 |
+
painter.drawRect(feature_rect)
|
| 1162 |
+
|
| 1163 |
+
# Draw label if enough space
|
| 1164 |
+
label = feature.get('name', 'HFL1')
|
| 1165 |
+
text_width = painter.fontMetrics().horizontalAdvance(label)
|
| 1166 |
+
if (x_end - x_start) > text_width:
|
| 1167 |
+
text_x = x_start + ((x_end - x_start) - text_width) / 2
|
| 1168 |
+
text_y = feature_y + self.feature_height/2 + 4
|
| 1169 |
+
painter.setPen(Qt.GlobalColor.white)
|
| 1170 |
+
painter.setFont(QFont("Arial", 8))
|
| 1171 |
+
painter.drawText(QPointF(text_x, text_y), label)
|
| 1172 |
+
|
| 1173 |
+
except Exception as e:
|
| 1174 |
+
if hasattr(self, 'logger'):
|
| 1175 |
+
self.logger.error(f"Error drawing feature: {str(e)}")
|
| 1176 |
+
continue
|
| 1177 |
+
|
| 1178 |
+
current_pos += self.bases_per_line
|
| 1179 |
+
|
| 1180 |
+
def boundingRect(self):
|
| 1181 |
+
if not self.sequence:
|
| 1182 |
+
return QRectF()
|
| 1183 |
+
|
| 1184 |
+
# Calculate exact width based on sequence length
|
| 1185 |
+
last_line_length = len(self.sequence) % self.bases_per_line
|
| 1186 |
+
if last_line_length == 0:
|
| 1187 |
+
last_line_length = self.bases_per_line
|
| 1188 |
+
width = max(self.base_width * self.bases_per_line,
|
| 1189 |
+
self.base_width * last_line_length) + 100
|
| 1190 |
+
|
| 1191 |
+
# Calculate height for actual sequence lines only
|
| 1192 |
+
total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
|
| 1193 |
+
height = total_lines * self.line_height * 2
|
| 1194 |
+
|
| 1195 |
+
return QRectF(0, 0, width, height)
|
| 1196 |
+
|
| 1197 |
+
def mousePressEvent(self, event):
|
| 1198 |
+
"""Handle mouse press to show insertion point"""
|
| 1199 |
+
if event.button() == Qt.MouseButton.LeftButton:
|
| 1200 |
+
# Calculate position based on click location
|
| 1201 |
+
local_pos = event.pos()
|
| 1202 |
+
line_number = int(local_pos.y() // (self.line_height * 2))
|
| 1203 |
+
base_position = int(local_pos.x() // self.base_width)
|
| 1204 |
+
|
| 1205 |
+
# Calculate absolute position
|
| 1206 |
+
position = self.start_pos + line_number * self.bases_per_line + base_position
|
| 1207 |
+
|
| 1208 |
+
# Emit cursor position
|
| 1209 |
+
self.cursor_position_changed.emit(position)
|
| 1210 |
+
|
| 1211 |
+
event.accept()
|
| 1212 |
+
|
| 1213 |
+
def mouseMoveEvent(self, event):
|
| 1214 |
+
"""Handle mouse move to update insertion point"""
|
| 1215 |
+
if event.buttons() & Qt.MouseButton.LeftButton:
|
| 1216 |
+
# Calculate position based on mouse location
|
| 1217 |
+
local_pos = event.pos()
|
| 1218 |
+
line_number = int(local_pos.y() // (self.line_height * 2))
|
| 1219 |
+
base_position = int(local_pos.x() // self.base_width)
|
| 1220 |
+
|
| 1221 |
+
# Calculate absolute position
|
| 1222 |
+
position = self.start_pos + line_number * self.bases_per_line + base_position
|
| 1223 |
+
|
| 1224 |
+
# Emit cursor position
|
| 1225 |
+
self.cursor_position_changed.emit(position)
|
| 1226 |
+
|
| 1227 |
+
event.accept()
|
|
@@ -1,7 +1,6 @@
|
|
| 1 |
from typing import Optional
|
| 2 |
from PyQt6.QtWidgets import QMainWindow
|
| 3 |
from PyQt6 import uic, QtWidgets
|
| 4 |
-
import os
|
| 5 |
|
| 6 |
class ExportSelectedgRNAsView(QMainWindow):
|
| 7 |
def __init__(self, global_settings):
|
|
@@ -14,6 +13,10 @@ class ExportSelectedgRNAsView(QMainWindow):
|
|
| 14 |
try:
|
| 15 |
uic.loadUi(self.settings.get_ui_dir_path() + '/export_selected_gRNAs.ui', self)
|
| 16 |
self.setWindowTitle("Export Selected gRNAs")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
self._init_ui_components()
|
| 18 |
except Exception as e:
|
| 19 |
self.logger.error(f"Error initializing ExportSelectedgRNAsView: {str(e)}", exc_info=True)
|
|
@@ -43,6 +46,13 @@ class ExportSelectedgRNAsView(QMainWindow):
|
|
| 43 |
return widget
|
| 44 |
|
| 45 |
def show_dialog(self) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
self.show()
|
| 47 |
self.activateWindow()
|
| 48 |
|
|
|
|
| 1 |
from typing import Optional
|
| 2 |
from PyQt6.QtWidgets import QMainWindow
|
| 3 |
from PyQt6 import uic, QtWidgets
|
|
|
|
| 4 |
|
| 5 |
class ExportSelectedgRNAsView(QMainWindow):
|
| 6 |
def __init__(self, global_settings):
|
|
|
|
| 13 |
try:
|
| 14 |
uic.loadUi(self.settings.get_ui_dir_path() + '/export_selected_gRNAs.ui', self)
|
| 15 |
self.setWindowTitle("Export Selected gRNAs")
|
| 16 |
+
|
| 17 |
+
# Set fixed size for the window
|
| 18 |
+
self.setFixedSize(500, 300) # Width: 500px, Height: 300px
|
| 19 |
+
|
| 20 |
self._init_ui_components()
|
| 21 |
except Exception as e:
|
| 22 |
self.logger.error(f"Error initializing ExportSelectedgRNAsView: {str(e)}", exc_info=True)
|
|
|
|
| 46 |
return widget
|
| 47 |
|
| 48 |
def show_dialog(self) -> None:
|
| 49 |
+
# Center the window on screen
|
| 50 |
+
screen = self.screen().availableGeometry()
|
| 51 |
+
self.move(
|
| 52 |
+
screen.center().x() - self.width() // 2,
|
| 53 |
+
screen.center().y() - self.height() // 2
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
self.show()
|
| 57 |
self.activateWindow()
|
| 58 |
|
|
@@ -9,6 +9,7 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 9 |
def __init__(self, global_settings):
|
| 10 |
super().__init__()
|
| 11 |
self.global_settings = global_settings
|
|
|
|
| 12 |
self._init_ui()
|
| 13 |
self.batch_size = 100 # Number of rows to load at once
|
| 14 |
self._all_results = [] # Store all results
|
|
@@ -67,31 +68,31 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 67 |
]
|
| 68 |
|
| 69 |
def display_results(self, results):
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
|
| 96 |
def _load_batch(self, start_idx, end_idx):
|
| 97 |
"""Load a batch of rows efficiently"""
|
|
@@ -120,7 +121,6 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 120 |
|
| 121 |
# Calculate which rows should be visible
|
| 122 |
scroll_position = value
|
| 123 |
-
start_row = max(0, scroll_position - visible_rows)
|
| 124 |
end_row = min(len(self._all_results), scroll_position + visible_rows * 2)
|
| 125 |
|
| 126 |
# Load more rows if needed
|
|
@@ -128,23 +128,46 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 128 |
self._load_batch(self._loaded_rows, end_row)
|
| 129 |
|
| 130 |
def get_selected_targets(self):
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
def clear_results(self):
|
| 141 |
-
"""Clear all results from the table"""
|
| 142 |
-
self.results_table.setUpdatesEnabled(False)
|
| 143 |
self.results_table.clearContents()
|
| 144 |
-
self.results_table.setRowCount(0)
|
| 145 |
-
self._all_results = []
|
| 146 |
-
self._loaded_rows = 0
|
| 147 |
-
self.results_table.setUpdatesEnabled(True)
|
| 148 |
|
| 149 |
def _on_generate_library_clicked(self):
|
| 150 |
"""Handle generate library button click"""
|
|
|
|
| 9 |
def __init__(self, global_settings):
|
| 10 |
super().__init__()
|
| 11 |
self.global_settings = global_settings
|
| 12 |
+
self.logger = global_settings.logger
|
| 13 |
self._init_ui()
|
| 14 |
self.batch_size = 100 # Number of rows to load at once
|
| 15 |
self._all_results = [] # Store all results
|
|
|
|
| 68 |
]
|
| 69 |
|
| 70 |
def display_results(self, results):
|
| 71 |
+
"""Display results with filtering support"""
|
| 72 |
+
try:
|
| 73 |
+
# Store all results and reset loaded count
|
| 74 |
+
self._all_results = results
|
| 75 |
+
self._loaded_rows = 0
|
| 76 |
+
|
| 77 |
+
# Disable visual updates
|
| 78 |
+
self.results_table.setUpdatesEnabled(False)
|
| 79 |
+
self.results_table.setSortingEnabled(False)
|
| 80 |
+
self.results_table.setVisible(False)
|
| 81 |
+
|
| 82 |
+
# Set total row count
|
| 83 |
+
total_rows = len(results)
|
| 84 |
+
self.results_table.setRowCount(total_rows)
|
| 85 |
+
|
| 86 |
+
# Load initial batch
|
| 87 |
+
self._load_batch(0, min(self.batch_size, total_rows))
|
| 88 |
+
|
| 89 |
+
# Re-enable table and updates
|
| 90 |
+
self.results_table.setVisible(True)
|
| 91 |
+
self.results_table.setUpdatesEnabled(True)
|
| 92 |
+
self.results_table.setSortingEnabled(True)
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
self.logger.error(f"Error displaying results: {str(e)}")
|
| 96 |
|
| 97 |
def _load_batch(self, start_idx, end_idx):
|
| 98 |
"""Load a batch of rows efficiently"""
|
|
|
|
| 121 |
|
| 122 |
# Calculate which rows should be visible
|
| 123 |
scroll_position = value
|
|
|
|
| 124 |
end_row = min(len(self._all_results), scroll_position + visible_rows * 2)
|
| 125 |
|
| 126 |
# Load more rows if needed
|
|
|
|
| 128 |
self._load_batch(self._loaded_rows, end_row)
|
| 129 |
|
| 130 |
def get_selected_targets(self):
|
| 131 |
+
"""Get selected targets from the currently displayed (filtered) results"""
|
| 132 |
+
try:
|
| 133 |
+
# Get indices of selected rows in the current view
|
| 134 |
+
selected_rows = set(index.row() for index in self.results_table.selectedIndexes())
|
| 135 |
+
selected_targets = []
|
| 136 |
+
|
| 137 |
+
# Get the currently visible rows from the table
|
| 138 |
+
visible_targets = []
|
| 139 |
+
for row in range(self.results_table.rowCount()):
|
| 140 |
+
if not self.results_table.isRowHidden(row):
|
| 141 |
+
# Get data from visible row
|
| 142 |
+
target_data = {
|
| 143 |
+
'feature_type': self.results_table.item(row, 0).text(),
|
| 144 |
+
'chromosome': self.results_table.item(row, 1).text(),
|
| 145 |
+
'feature_id': self.results_table.item(row, 2).text(),
|
| 146 |
+
'feature_name': self.results_table.item(row, 3).text(),
|
| 147 |
+
'feature_description': self.results_table.item(row, 4).text()
|
| 148 |
+
}
|
| 149 |
+
visible_targets.append((row, target_data))
|
| 150 |
+
|
| 151 |
+
# Match selected rows with visible targets
|
| 152 |
+
for row, target_data in visible_targets:
|
| 153 |
+
if row in selected_rows:
|
| 154 |
+
# Find corresponding full target data from _all_results
|
| 155 |
+
for full_target in self._all_results:
|
| 156 |
+
if (full_target['feature_id'] == target_data['feature_id'] and
|
| 157 |
+
full_target['feature_type'] == target_data['feature_type']):
|
| 158 |
+
selected_targets.append(full_target)
|
| 159 |
+
break
|
| 160 |
|
| 161 |
+
self.logger.debug(f"Selected {len(selected_targets)} targets from filtered view")
|
| 162 |
+
return selected_targets
|
| 163 |
+
|
| 164 |
+
except Exception as e:
|
| 165 |
+
self.logger.error(f"Error getting selected targets: {str(e)}")
|
| 166 |
+
return []
|
| 167 |
+
|
| 168 |
def clear_results(self):
|
|
|
|
|
|
|
| 169 |
self.results_table.clearContents()
|
| 170 |
+
self.results_table.setRowCount(0)
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
def _on_generate_library_clicked(self):
|
| 173 |
"""Handle generate library button click"""
|
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
__author__ = 'brianmendoza'
|
| 2 |
-
|
| 3 |
-
from Bio import Entrez, SeqIO
|
| 4 |
-
import webbrowser
|
| 5 |
-
import re
|
| 6 |
-
import os
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class GenBankFile:
|
| 10 |
-
|
| 11 |
-
def __init__(self, organism):
|
| 12 |
-
Entrez.email = "bmendoz1@vols.utk.edu"
|
| 13 |
-
self.directory = "/Users/brianmendoza/Desktop/GenBank_files/"
|
| 14 |
-
self.org = organism
|
| 15 |
-
|
| 16 |
-
def setOrg(self, org):
|
| 17 |
-
self.org = org
|
| 18 |
-
|
| 19 |
-
def setDirectory(self, path, org):
|
| 20 |
-
self.directory = path
|
| 21 |
-
self.setOrg(org)
|
| 22 |
-
|
| 23 |
-
def convertToFasta(self):
|
| 24 |
-
orgfile = self.directory + self.org + ".gbff"
|
| 25 |
-
output = "/Users/brianmendoza/Desktop/GenBank_files/FASTAs/" + self.org + ".fna"
|
| 26 |
-
SeqIO.convert(orgfile, "genbank", output, "fasta")
|
| 27 |
-
|
| 28 |
-
def parseAnnotation(self):
|
| 29 |
-
gb_file = self.directory + self.org + ".gbff"
|
| 30 |
-
records = SeqIO.parse(open(gb_file,"r"), "genbank")
|
| 31 |
-
|
| 32 |
-
# create table for multi-targeting reference
|
| 33 |
-
table = {}
|
| 34 |
-
count = 0
|
| 35 |
-
for record in records:
|
| 36 |
-
count += 1
|
| 37 |
-
chrmnumber = str(count)
|
| 38 |
-
table[chrmnumber] = []
|
| 39 |
-
for feature in record.features:
|
| 40 |
-
if feature.type == 'CDS': # hopefully gene and CDS are the same
|
| 41 |
-
|
| 42 |
-
# getting the location...
|
| 43 |
-
loc = str(feature.location)
|
| 44 |
-
out = re.findall(r"[\d]+", loc)
|
| 45 |
-
start = out[0]
|
| 46 |
-
end = out[1]
|
| 47 |
-
if len(out) > 2: # to account for "joined" domains
|
| 48 |
-
end = out[3]
|
| 49 |
-
|
| 50 |
-
# locus_tag and product...
|
| 51 |
-
if 'locus_tag' in feature.qualifiers:
|
| 52 |
-
ltag = feature.qualifiers['locus_tag']
|
| 53 |
-
elif 'gene' in feature.qualifiers:
|
| 54 |
-
ltag = feature.qualifiers['gene']
|
| 55 |
-
if 'product' not in feature.qualifiers:
|
| 56 |
-
prod = feature.qualifiers['note']
|
| 57 |
-
else:
|
| 58 |
-
prod = feature.qualifiers['product']
|
| 59 |
-
# adding it all up...
|
| 60 |
-
tup = (start, end, ltag, prod)
|
| 61 |
-
table[chrmnumber].append(tup)
|
| 62 |
-
return table
|
| 63 |
-
|
| 64 |
-
def getChromSequence(self, index):
|
| 65 |
-
gb_file = self.directory + self.org + ".gbff"
|
| 66 |
-
records = SeqIO.parse(open(gb_file,"r"), "genbank")
|
| 67 |
-
count = 0
|
| 68 |
-
for record in records:
|
| 69 |
-
count += 1
|
| 70 |
-
if count == index:
|
| 71 |
-
cstr = record.seq
|
| 72 |
-
return cstr
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
class GffFile:
|
| 76 |
-
|
| 77 |
-
def __init__(self, organism):
|
| 78 |
-
self.directory = "/Users/brianmendoza/Desktop/GenBank_Files/"
|
| 79 |
-
self.org = organism
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -103,10 +103,11 @@ class HomeWindowView(QWidget):
|
|
| 103 |
# self.combo_box_local_annotation_files.addItems(annotation_files)
|
| 104 |
|
| 105 |
def get_find_targets_input(self) -> dict:
|
|
|
|
| 106 |
return {
|
| 107 |
"organism": self.combo_box_organism.currentText(),
|
| 108 |
"endonuclease": self.combo_box_endonuclease.currentText(),
|
| 109 |
-
"annotation_file":
|
| 110 |
"search_type": self.get_search_type(),
|
| 111 |
"search_query": self.text_edit_gene_entry.toPlainText()
|
| 112 |
}
|
|
@@ -130,13 +131,19 @@ class HomeWindowView(QWidget):
|
|
| 130 |
# Clear existing items
|
| 131 |
self.combo_box_local_annotation_files.clear()
|
| 132 |
|
| 133 |
-
# Filter out .index files
|
| 134 |
-
filtered_files = [
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
# Add filtered files to combo box
|
| 137 |
if filtered_files:
|
| 138 |
self.combo_box_local_annotation_files.addItems(filtered_files)
|
|
|
|
| 139 |
self.combo_box_local_annotation_files.setCurrentIndex(0)
|
|
|
|
|
|
|
| 140 |
self.logger.debug(f"Added {len(filtered_files)} local annotation files to combo box")
|
| 141 |
else:
|
| 142 |
self.logger.debug("No local annotation files found")
|
|
@@ -144,6 +151,18 @@ class HomeWindowView(QWidget):
|
|
| 144 |
except Exception as e:
|
| 145 |
self.logger.error(f"Error updating local annotation files: {str(e)}")
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
def show_warning(self, title: str, message: str) -> None:
|
| 148 |
"""Show a warning message dialog"""
|
| 149 |
QtWidgets.QMessageBox.warning(self, title, message)
|
|
|
|
| 103 |
# self.combo_box_local_annotation_files.addItems(annotation_files)
|
| 104 |
|
| 105 |
def get_find_targets_input(self) -> dict:
|
| 106 |
+
current_annotation = self.combo_box_local_annotation_files.currentText()
|
| 107 |
return {
|
| 108 |
"organism": self.combo_box_organism.currentText(),
|
| 109 |
"endonuclease": self.combo_box_endonuclease.currentText(),
|
| 110 |
+
"annotation_file": current_annotation,
|
| 111 |
"search_type": self.get_search_type(),
|
| 112 |
"search_query": self.text_edit_gene_entry.toPlainText()
|
| 113 |
}
|
|
|
|
| 131 |
# Clear existing items
|
| 132 |
self.combo_box_local_annotation_files.clear()
|
| 133 |
|
| 134 |
+
# Filter out .index files and ensure files are valid
|
| 135 |
+
filtered_files = [
|
| 136 |
+
f for f in files
|
| 137 |
+
if not f.endswith('.index') and f.strip()
|
| 138 |
+
]
|
| 139 |
|
| 140 |
# Add filtered files to combo box
|
| 141 |
if filtered_files:
|
| 142 |
self.combo_box_local_annotation_files.addItems(filtered_files)
|
| 143 |
+
# Set the first item as current
|
| 144 |
self.combo_box_local_annotation_files.setCurrentIndex(0)
|
| 145 |
+
# Emit the change signal to update the current annotation file
|
| 146 |
+
self._on_annotation_file_changed(self.combo_box_local_annotation_files.currentText())
|
| 147 |
self.logger.debug(f"Added {len(filtered_files)} local annotation files to combo box")
|
| 148 |
else:
|
| 149 |
self.logger.debug("No local annotation files found")
|
|
|
|
| 151 |
except Exception as e:
|
| 152 |
self.logger.error(f"Error updating local annotation files: {str(e)}")
|
| 153 |
|
| 154 |
+
def _on_annotation_file_changed(self, new_file):
|
| 155 |
+
"""Handle annotation file changes"""
|
| 156 |
+
try:
|
| 157 |
+
if new_file:
|
| 158 |
+
self.logger.debug(f"Setting current annotation file to: {new_file}")
|
| 159 |
+
self.global_settings.set_current_annotation_file(new_file)
|
| 160 |
+
# Ensure the combo box reflects the current selection
|
| 161 |
+
if self.combo_box_local_annotation_files.currentText() != new_file:
|
| 162 |
+
self.combo_box_local_annotation_files.setCurrentText(new_file)
|
| 163 |
+
except Exception as e:
|
| 164 |
+
self.logger.error(f"Error handling annotation file change: {str(e)}")
|
| 165 |
+
|
| 166 |
def show_warning(self, title: str, message: str) -> None:
|
| 167 |
"""Show a warning message dialog"""
|
| 168 |
QtWidgets.QMessageBox.warning(self, title, message)
|
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import QDialog, QProgressBar, QVBoxLayout, QLabel
|
| 2 |
+
from PyQt6.QtCore import Qt
|
| 3 |
+
from PyQt6.QtWidgets import QApplication
|
| 4 |
+
|
| 5 |
+
class LoadingDialog(QDialog):
|
| 6 |
+
def __init__(self, parent=None, message="Loading..."):
|
| 7 |
+
super().__init__(parent)
|
| 8 |
+
self.setWindowTitle("Please Wait")
|
| 9 |
+
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
| 10 |
+
self.setFixedSize(300, 100)
|
| 11 |
+
|
| 12 |
+
# Remove window decorations and set dialog flags
|
| 13 |
+
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
|
| 14 |
+
|
| 15 |
+
# Create layout
|
| 16 |
+
layout = QVBoxLayout()
|
| 17 |
+
|
| 18 |
+
# Add message label
|
| 19 |
+
self.label = QLabel(message)
|
| 20 |
+
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
| 21 |
+
layout.addWidget(self.label)
|
| 22 |
+
|
| 23 |
+
self.progress_bar = QProgressBar()
|
| 24 |
+
self.progress_bar.setRange(0, 100) # Set range for percentage
|
| 25 |
+
layout.addWidget(self.progress_bar)
|
| 26 |
+
|
| 27 |
+
self.setLayout(layout)
|
| 28 |
+
|
| 29 |
+
# Center on main window
|
| 30 |
+
self.center_on_parent()
|
| 31 |
+
|
| 32 |
+
def center_on_parent(self):
|
| 33 |
+
"""Center the dialog on the main window or parent"""
|
| 34 |
+
parent = self.parent()
|
| 35 |
+
if parent:
|
| 36 |
+
# Get the main window from global settings if available
|
| 37 |
+
main_window = None
|
| 38 |
+
if hasattr(parent, 'global_settings'):
|
| 39 |
+
main_window = parent.global_settings.main_window
|
| 40 |
+
elif hasattr(parent, 'settings'):
|
| 41 |
+
main_window = parent.settings.main_window
|
| 42 |
+
|
| 43 |
+
# Get geometry of the window to center on
|
| 44 |
+
if main_window and main_window.view:
|
| 45 |
+
geometry = main_window.view.geometry()
|
| 46 |
+
else:
|
| 47 |
+
geometry = parent.geometry()
|
| 48 |
+
|
| 49 |
+
# Calculate center position
|
| 50 |
+
x = geometry.x() + (geometry.width() - self.width()) // 2
|
| 51 |
+
y = geometry.y() + (geometry.height() - self.height()) // 2
|
| 52 |
+
|
| 53 |
+
# Ensure dialog stays within screen bounds
|
| 54 |
+
screen = QApplication.primaryScreen().geometry()
|
| 55 |
+
x = max(screen.left(), min(x, screen.right() - self.width()))
|
| 56 |
+
y = max(screen.top(), min(y, screen.bottom() - self.height()))
|
| 57 |
+
|
| 58 |
+
self.move(x, y)
|
| 59 |
+
|
| 60 |
+
def set_message(self, message, progress=None):
|
| 61 |
+
"""Update the loading message and optionally the progress"""
|
| 62 |
+
if progress is not None:
|
| 63 |
+
self.progress_bar.setValue(progress)
|
| 64 |
+
self.label.setText(message)
|
| 65 |
+
|
| 66 |
+
# Recenter after updating message
|
| 67 |
+
self.center_on_parent()
|
| 68 |
+
|
| 69 |
+
def set_progress(self, value):
|
| 70 |
+
"""Set progress value (0-100)"""
|
| 71 |
+
self.progress_bar.setValue(value)
|
| 72 |
+
self.label.setText("Loading...")
|
| 73 |
+
|
| 74 |
+
def set_indeterminate(self):
|
| 75 |
+
"""Set indeterminate progress"""
|
| 76 |
+
self.progress_bar.setRange(0, 0)
|
| 77 |
+
self.label.setText("Loading...")
|
| 78 |
+
|
| 79 |
+
def showEvent(self, event):
|
| 80 |
+
"""Override show event to ensure dialog is centered when shown"""
|
| 81 |
+
super().showEvent(event)
|
| 82 |
+
self.center_on_parent()
|
| 83 |
+
|
|
@@ -1,152 +0,0 @@
|
|
| 1 |
-
from PyQt6 import QtWidgets, QtGui, QtCore, uic
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
class MainWindowUI(QtWidgets.QMainWindow):
|
| 5 |
-
def __init__(self, settings):
|
| 6 |
-
super(MainWindowUI, self).__init__()
|
| 7 |
-
self.settings = settings
|
| 8 |
-
self.setup_ui()
|
| 9 |
-
|
| 10 |
-
def setup_ui(self):
|
| 11 |
-
# Load the UI file
|
| 12 |
-
uic.loadUi(os.path.join(self.settings.get_ui_dir(), 'main_window.ui'), self)
|
| 13 |
-
|
| 14 |
-
# Set window properties
|
| 15 |
-
self.setWindowTitle("CASPER")
|
| 16 |
-
self.setWindowIcon(QtGui.QIcon(os.path.join(self.settings.get_assets_dir(), "cas9image.ico")))
|
| 17 |
-
|
| 18 |
-
# Initialize UI components
|
| 19 |
-
self.init_ui_components()
|
| 20 |
-
|
| 21 |
-
# Set up styles
|
| 22 |
-
self.set_styles()
|
| 23 |
-
|
| 24 |
-
# Initialize progress bar to 0
|
| 25 |
-
self.progressBar.setValue(0)
|
| 26 |
-
|
| 27 |
-
# Add the theme toggle button
|
| 28 |
-
self.theme_toggle_button = QtWidgets.QPushButton(self)
|
| 29 |
-
self.theme_toggle_button.setFixedSize(32, 32)
|
| 30 |
-
self.theme_toggle_button.setStyleSheet("border: none;")
|
| 31 |
-
self.update_theme_icon()
|
| 32 |
-
|
| 33 |
-
# Position the button in the top right corner
|
| 34 |
-
self.theme_toggle_button.setGeometry(self.width() - 40, 10, 32, 32)
|
| 35 |
-
|
| 36 |
-
# Connect the button to a slot (to be implemented in the controller)
|
| 37 |
-
self.theme_toggle_button.clicked.connect(self.on_theme_toggle)
|
| 38 |
-
|
| 39 |
-
def init_ui_components(self):
|
| 40 |
-
# Initialize and find all the UI components
|
| 41 |
-
self.org_choice = self.findChild(QtWidgets.QComboBox, 'orgChoice')
|
| 42 |
-
self.endo_choice = self.findChild(QtWidgets.QComboBox, 'endoChoice')
|
| 43 |
-
self.annotation_files = self.findChild(QtWidgets.QComboBox, 'annotation_files')
|
| 44 |
-
self.gene_entry_field = self.findChild(QtWidgets.QPlainTextEdit, 'gene_entry_field')
|
| 45 |
-
|
| 46 |
-
self.push_button_find_targets = self.findChild(QtWidgets.QPushButton, 'pushButton_FindTargets')
|
| 47 |
-
self.push_button_view_targets = self.findChild(QtWidgets.QPushButton, 'pushButton_ViewTargets')
|
| 48 |
-
self.generate_library = self.findChild(QtWidgets.QPushButton, 'GenerateLibrary')
|
| 49 |
-
|
| 50 |
-
self.radio_button_gene = self.findChild(QtWidgets.QRadioButton, 'radioButton_Gene')
|
| 51 |
-
self.radio_button_position = self.findChild(QtWidgets.QRadioButton, 'radioButton_Position')
|
| 52 |
-
self.radio_button_sequence = self.findChild(QtWidgets.QRadioButton, 'radioButton_Sequence')
|
| 53 |
-
|
| 54 |
-
self.new_genome_button = self.findChild(QtWidgets.QPushButton, 'newGenome_button')
|
| 55 |
-
self.new_endo_button = self.findChild(QtWidgets.QPushButton, 'newEndo_button')
|
| 56 |
-
self.multitargeting_button = self.findChild(QtWidgets.QPushButton, 'multitargeting_button')
|
| 57 |
-
self.population_analysis_button = self.findChild(QtWidgets.QPushButton, 'populationAnalysis_button')
|
| 58 |
-
self.combine_files_button = self.findChild(QtWidgets.QPushButton, 'combineFiles_button')
|
| 59 |
-
|
| 60 |
-
self.progress_bar = self.findChild(QtWidgets.QProgressBar, 'progressBar')
|
| 61 |
-
|
| 62 |
-
self.step1 = self.findChild(QtWidgets.QGroupBox, 'Step1')
|
| 63 |
-
self.step2 = self.findChild(QtWidgets.QGroupBox, 'Step2')
|
| 64 |
-
self.step3 = self.findChild(QtWidgets.QGroupBox, 'Step3')
|
| 65 |
-
self.casper_navigation = self.findChild(QtWidgets.QGroupBox, 'CASPER_Navigation')
|
| 66 |
-
|
| 67 |
-
self.ncbi_button = self.findChild(QtWidgets.QPushButton, 'ncbi_button')
|
| 68 |
-
|
| 69 |
-
# Connect the actionChange_Directory to a slot
|
| 70 |
-
self.actionChange_Directory.triggered.connect(self.on_change_directory)
|
| 71 |
-
|
| 72 |
-
def set_styles(self):
|
| 73 |
-
groupbox_style = """
|
| 74 |
-
QGroupBox:title{subcontrol-origin: margin;
|
| 75 |
-
left: 10px;
|
| 76 |
-
padding: 0 5px 0 5px;}
|
| 77 |
-
QGroupBox#Step1{border: 2px solid rgb(111,181,110);
|
| 78 |
-
border-radius: 9px;
|
| 79 |
-
margin-top: 10px;
|
| 80 |
-
font: bold 14pt 'Arial';}
|
| 81 |
-
"""
|
| 82 |
-
self.step1.setStyleSheet(groupbox_style)
|
| 83 |
-
self.step2.setStyleSheet(groupbox_style.replace("Step1", "Step2"))
|
| 84 |
-
self.step3.setStyleSheet(groupbox_style.replace("Step1", "Step3"))
|
| 85 |
-
self.casper_navigation.setStyleSheet(groupbox_style.replace("Step1", "CASPER_Navigation")
|
| 86 |
-
.replace("solid","dashed")
|
| 87 |
-
.replace("rgb(111,181,110)","rgb(88,89,91)"))
|
| 88 |
-
|
| 89 |
-
def set_gene_entry_placeholder(self):
|
| 90 |
-
placeholder_text = ("Example Inputs: \n\n"
|
| 91 |
-
"Option 1: Feature (ID, Locus Tag, or Name)\n"
|
| 92 |
-
"Example: 854068/YOL086C/ADH1 for S. cerevisiae alcohol dehydrogenase 1\n\n"
|
| 93 |
-
"Option 2: Position (chromosome,start,stop)\n"
|
| 94 |
-
"Example: 1,1,1000 for targeting chromosome 1, base pairs 1 to 1000\n\n"
|
| 95 |
-
"Option 3: Sequence (must be within the selected organism)\n"
|
| 96 |
-
"Example: Any nucleotide sequence between 100 and 10,000 base pairs.\n\n"
|
| 97 |
-
"*Note: to multiplex, separate multiple queries by new lines*\n"
|
| 98 |
-
"Example:\n"
|
| 99 |
-
"1,1,1000\n"
|
| 100 |
-
"5,1,500\n"
|
| 101 |
-
"etc.")
|
| 102 |
-
self.gene_entry_field.setPlaceholderText(placeholder_text)
|
| 103 |
-
|
| 104 |
-
def enable_view_targets(self, enable):
|
| 105 |
-
self.push_button_view_targets.setEnabled(enable)
|
| 106 |
-
|
| 107 |
-
def enable_generate_library(self, enable):
|
| 108 |
-
self.generate_library.setEnabled(enable)
|
| 109 |
-
|
| 110 |
-
def set_progress(self, value):
|
| 111 |
-
if self.progressBar:
|
| 112 |
-
self.progressBar.setValue(value)
|
| 113 |
-
|
| 114 |
-
def reset_progress(self):
|
| 115 |
-
if self.progressBar:
|
| 116 |
-
self.progressBar.setValue(0)
|
| 117 |
-
|
| 118 |
-
def toggle_annotation(self, gene_checked):
|
| 119 |
-
self.step2.setEnabled(True)
|
| 120 |
-
|
| 121 |
-
def update_endo_choice(self, endos):
|
| 122 |
-
self.endo_choice.clear()
|
| 123 |
-
self.endo_choice.addItems(endos)
|
| 124 |
-
|
| 125 |
-
def bring_to_front(self):
|
| 126 |
-
self.show()
|
| 127 |
-
self.setWindowState(self.windowState() & ~QtCore.Qt.WindowState.WindowMinimized | QtCore.Qt.WindowState.WindowActive)
|
| 128 |
-
self.raise_()
|
| 129 |
-
self.activateWindow()
|
| 130 |
-
QtWidgets.QApplication.setActiveWindow(self)
|
| 131 |
-
|
| 132 |
-
def on_change_directory(self):
|
| 133 |
-
# This method will be connected to the controller
|
| 134 |
-
pass
|
| 135 |
-
|
| 136 |
-
def update_theme_icon(self):
|
| 137 |
-
# Swap the icons: use dark_mode.png for light mode, and light_mode.png for dark mode
|
| 138 |
-
icon = QtGui.QIcon(os.path.join(self.settings.get_assets_dir(), "dark_mode.png" if self.settings.is_dark_mode() else "light_mode.png"))
|
| 139 |
-
self.theme_toggle_button.setIcon(icon)
|
| 140 |
-
self.theme_toggle_button.setIconSize(QtCore.QSize(24, 24))
|
| 141 |
-
|
| 142 |
-
# Update the entire application's theme
|
| 143 |
-
self.settings.apply_theme()
|
| 144 |
-
|
| 145 |
-
def resizeEvent(self, event):
|
| 146 |
-
super().resizeEvent(event)
|
| 147 |
-
# Reposition the theme toggle button when the window is resized
|
| 148 |
-
self.theme_toggle_button.setGeometry(self.width() - 40, 10, 32, 32)
|
| 149 |
-
|
| 150 |
-
def on_theme_toggle(self):
|
| 151 |
-
# This method will be connected to the controller
|
| 152 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,265 +0,0 @@
|
|
| 1 |
-
from PyQt6.QtWidgets import QMainWindow, QPushButton, QRadioButton, QComboBox, QPlainTextEdit, QProgressBar, QMenuBar, QMenu, QStackedWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame
|
| 2 |
-
from PyQt6.QtGui import QIcon, QAction, QFont
|
| 3 |
-
from PyQt6.QtCore import Qt, QPoint
|
| 4 |
-
from PyQt6 import uic, QtWidgets, QtCore
|
| 5 |
-
from utils.ui import scale_ui, show_error
|
| 6 |
-
import os
|
| 7 |
-
from typing import Optional
|
| 8 |
-
|
| 9 |
-
class MainWindowView(QMainWindow):
|
| 10 |
-
def __init__(self, global_settings):
|
| 11 |
-
super().__init__()
|
| 12 |
-
self.global_settings = global_settings
|
| 13 |
-
self._init_ui()
|
| 14 |
-
|
| 15 |
-
def _init_ui(self) -> None:
|
| 16 |
-
try:
|
| 17 |
-
# self._load_ui_file()
|
| 18 |
-
self._init_window_properties()
|
| 19 |
-
self._init_custom_title_bar()
|
| 20 |
-
self._init_ui_elements()
|
| 21 |
-
self._scale_ui()
|
| 22 |
-
except Exception as e:
|
| 23 |
-
self._handle_init_error(e)
|
| 24 |
-
|
| 25 |
-
def _load_ui_file(self) -> None:
|
| 26 |
-
ui_file = os.path.join(self.global_settings.get_ui_dir(), "home_window.ui")
|
| 27 |
-
uic.loadUi(ui_file, self)
|
| 28 |
-
|
| 29 |
-
def _init_window_properties(self) -> None:
|
| 30 |
-
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
|
| 31 |
-
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
| 32 |
-
|
| 33 |
-
def _init_ui_elements(self) -> None:
|
| 34 |
-
# Create a main widget to hold everything
|
| 35 |
-
main_widget = QWidget()
|
| 36 |
-
main_layout = QVBoxLayout(main_widget)
|
| 37 |
-
main_layout.setContentsMargins(0, 0, 0, 0)
|
| 38 |
-
main_layout.setSpacing(0)
|
| 39 |
-
|
| 40 |
-
# Add the custom title bar
|
| 41 |
-
main_layout.addWidget(self.title_bar)
|
| 42 |
-
|
| 43 |
-
# Create and add the divider
|
| 44 |
-
divider = QFrame()
|
| 45 |
-
divider.setFrameShape(QFrame.Shape.HLine)
|
| 46 |
-
divider.setFrameShadow(QFrame.Shadow.Sunken)
|
| 47 |
-
divider.setStyleSheet("background-color: #c0c0c0;") # Light gray color
|
| 48 |
-
divider.setFixedHeight(1) # 1 pixel height
|
| 49 |
-
main_layout.addWidget(divider)
|
| 50 |
-
|
| 51 |
-
# Create a widget to hold the original content
|
| 52 |
-
content_widget = QWidget()
|
| 53 |
-
content_layout = QVBoxLayout(content_widget)
|
| 54 |
-
|
| 55 |
-
# Move the existing widgets to the content layout
|
| 56 |
-
for child in self.children():
|
| 57 |
-
if isinstance(child, (QMenuBar, QWidget)) and child != self.title_bar:
|
| 58 |
-
content_layout.addWidget(child)
|
| 59 |
-
|
| 60 |
-
# Create the stacked widget
|
| 61 |
-
self.stacked_widget = QStackedWidget()
|
| 62 |
-
|
| 63 |
-
# Add the content widget and stacked widget to the main layout
|
| 64 |
-
main_layout.addWidget(content_widget)
|
| 65 |
-
main_layout.addWidget(self.stacked_widget)
|
| 66 |
-
|
| 67 |
-
# Set the main widget as the central widget
|
| 68 |
-
self.setCentralWidget(main_widget)
|
| 69 |
-
|
| 70 |
-
# Initialize other UI elements
|
| 71 |
-
self._init_menuBar()
|
| 72 |
-
self._init_grpNavigationMenu()
|
| 73 |
-
self._init_grpStep1()
|
| 74 |
-
self._init_grpStep2()
|
| 75 |
-
self._init_grpStep3()
|
| 76 |
-
|
| 77 |
-
def _init_custom_title_bar(self) -> None:
|
| 78 |
-
self.title_bar = QWidget(self)
|
| 79 |
-
self.title_bar.setObjectName("custom_title_bar")
|
| 80 |
-
self.title_bar.setFixedHeight(32) # Reduced height
|
| 81 |
-
|
| 82 |
-
# Create the main horizontal layout for the title bar
|
| 83 |
-
layout = QHBoxLayout(self.title_bar)
|
| 84 |
-
layout.setContentsMargins(10, 0, 10, 0) # Equal margins on left and right
|
| 85 |
-
layout.setSpacing(5) # Reduced spacing between items
|
| 86 |
-
|
| 87 |
-
# ----- Window Control Buttons -----
|
| 88 |
-
button_font = QFont("Arial", 8) # Smaller font size for button text
|
| 89 |
-
|
| 90 |
-
self.minimize_button = QPushButton("-", self.title_bar)
|
| 91 |
-
self.minimize_button.setObjectName("minimize_button")
|
| 92 |
-
self.minimize_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
|
| 93 |
-
self.minimize_button.setFont(button_font)
|
| 94 |
-
|
| 95 |
-
self.maximize_button = QPushButton("⛶", self.title_bar)
|
| 96 |
-
self.maximize_button.setObjectName("maximize_button")
|
| 97 |
-
self.maximize_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
|
| 98 |
-
self.maximize_button.setFont(button_font)
|
| 99 |
-
|
| 100 |
-
self.close_button = QPushButton("✕", self.title_bar)
|
| 101 |
-
self.close_button.setObjectName("close_button")
|
| 102 |
-
self.close_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
|
| 103 |
-
self.close_button.setFont(button_font)
|
| 104 |
-
|
| 105 |
-
# Apply a style to center the text vertically and horizontally
|
| 106 |
-
button_style = """
|
| 107 |
-
QPushButton {
|
| 108 |
-
padding: 0px;
|
| 109 |
-
margin: 0px;
|
| 110 |
-
line-height: 20px;
|
| 111 |
-
text-align: center;
|
| 112 |
-
}
|
| 113 |
-
"""
|
| 114 |
-
self.minimize_button.setStyleSheet(button_style)
|
| 115 |
-
self.maximize_button.setStyleSheet(button_style)
|
| 116 |
-
self.close_button.setStyleSheet(button_style)
|
| 117 |
-
|
| 118 |
-
# ----- Left Widget (Minimize, Maximize, Close) -----
|
| 119 |
-
left_widget = QWidget()
|
| 120 |
-
left_layout = QHBoxLayout(left_widget)
|
| 121 |
-
left_layout.setContentsMargins(0, 0, 0, 0)
|
| 122 |
-
left_layout.setSpacing(5)
|
| 123 |
-
left_layout.addWidget(self.close_button)
|
| 124 |
-
left_layout.addWidget(self.minimize_button)
|
| 125 |
-
left_layout.addWidget(self.maximize_button)
|
| 126 |
-
|
| 127 |
-
# ----- Theme Toggle Button -----
|
| 128 |
-
self.theme_toggle_button = QPushButton(self.title_bar)
|
| 129 |
-
self.theme_toggle_button.setObjectName("theme_toggle_button")
|
| 130 |
-
self.theme_toggle_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
|
| 131 |
-
self.theme_toggle_button.setStyleSheet("border: none;")
|
| 132 |
-
self.update_theme_icon()
|
| 133 |
-
|
| 134 |
-
# ----- Right Widget (Theme Toggle + Stretch) -----
|
| 135 |
-
right_widget = QWidget()
|
| 136 |
-
right_layout = QHBoxLayout(right_widget)
|
| 137 |
-
right_layout.setContentsMargins(0, 0, 0, 0)
|
| 138 |
-
right_layout.setSpacing(5)
|
| 139 |
-
|
| 140 |
-
# Add a stretch to push the toggle button to the far right within right_widget
|
| 141 |
-
right_layout.addStretch()
|
| 142 |
-
right_layout.addWidget(self.theme_toggle_button)
|
| 143 |
-
|
| 144 |
-
# ----- Synchronize Widths of Left and Right Widgets -----
|
| 145 |
-
# Adjust left_widget to calculate its required width
|
| 146 |
-
left_widget.adjustSize()
|
| 147 |
-
left_width = left_widget.sizeHint().width()
|
| 148 |
-
|
| 149 |
-
# Set right_widget's fixed width to match left_widget's width
|
| 150 |
-
right_widget.setFixedWidth(left_width)
|
| 151 |
-
|
| 152 |
-
# ----- Title Label -----
|
| 153 |
-
self.title_label = QLabel("CASPER", self.title_bar)
|
| 154 |
-
self.title_label.setObjectName("title_label")
|
| 155 |
-
self.title_label.setFont(QFont("Arial", 10, QFont.Weight.Bold)) # Reduced font size
|
| 156 |
-
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the text in the label
|
| 157 |
-
self.title_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred)
|
| 158 |
-
|
| 159 |
-
# ----- Add Widgets to the Main Title Bar Layout -----
|
| 160 |
-
layout.addWidget(left_widget) # Left side buttons
|
| 161 |
-
layout.addStretch(1) # Stretchable space
|
| 162 |
-
layout.addWidget(self.title_label) # Centered title
|
| 163 |
-
layout.addStretch(1) # Stretchable space
|
| 164 |
-
layout.addWidget(right_widget) # Right side buttons (theme toggle + stretch)
|
| 165 |
-
|
| 166 |
-
# Optional: Ensure the title_label is truly centered
|
| 167 |
-
self.title_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred)
|
| 168 |
-
def _init_menuBar(self) -> None:
|
| 169 |
-
self.action_change_directory = self._find_widget("actChangeDirectory", QAction)
|
| 170 |
-
self.action_open_genome_browser = self._find_widget("actOpenGenomeBrowser", QAction)
|
| 171 |
-
self.action_open_repository = self._find_widget("actOpenRepository", QAction)
|
| 172 |
-
self.action_open_NCBI_BLAST = self._find_widget("actOpenNCBIBLAST", QAction)
|
| 173 |
-
self.action_open_NCBI = self._find_widget("actOpenNCBI", QAction)
|
| 174 |
-
|
| 175 |
-
def _init_grpNavigationMenu(self) -> None:
|
| 176 |
-
self.push_button_new_genome = self._find_widget("pbtnNewGenome", QPushButton)
|
| 177 |
-
self.push_button_new_endonuclease = self._find_widget("pbtnNewEndonuclease", QPushButton)
|
| 178 |
-
self.push_button_multitargeting_analysis = self._find_widget("pbtnMultitargetingAnalysis", QPushButton)
|
| 179 |
-
self.push_button_population_analysis = self._find_widget("pbtnPopulationAnalysis", QPushButton)
|
| 180 |
-
self.push_button_combine_files = self._find_widget("pbtnCombineFiles", QPushButton)
|
| 181 |
-
|
| 182 |
-
def _init_grpStep1(self) -> None:
|
| 183 |
-
self.combo_box_organism = self._find_widget("cmbOrganism", QComboBox)
|
| 184 |
-
self.combo_box_endonuclease = self._find_widget("cmbEndonuclease", QComboBox)
|
| 185 |
-
|
| 186 |
-
def _init_grpStep2(self) -> None:
|
| 187 |
-
self.push_button_ncbi_file_search = self._find_widget("pbtnNCBIFileSearch", QPushButton)
|
| 188 |
-
self.combo_box_local_annotation_files = self._find_widget("cmbLocalAnnotationFiles", QComboBox)
|
| 189 |
-
|
| 190 |
-
def _init_grpStep3(self) -> None:
|
| 191 |
-
self.radio_button_feature = self._find_widget("rbtnFeature", QRadioButton)
|
| 192 |
-
self.radio_button_position = self._find_widget("rbtnPosition", QRadioButton)
|
| 193 |
-
self.radio_button_sequence = self._find_widget("rbtnSequence", QRadioButton)
|
| 194 |
-
self.text_edit_gene_entry = self._find_widget("txtedGeneEntry", QPlainTextEdit)
|
| 195 |
-
self.push_button_find_targets = self._find_widget("pbtnFindTargets", QPushButton)
|
| 196 |
-
self.progress_bar_find_targets = self._find_widget("progBarFindTargets", QProgressBar)
|
| 197 |
-
self.push_button_view_targets = self._find_widget("pbtnViewTargets", QPushButton)
|
| 198 |
-
self.push_button_generate_library = self._find_widget("pbtnGenerateLibrary", QPushButton)
|
| 199 |
-
|
| 200 |
-
placeholder_text = ("Example Inputs: \n\n"
|
| 201 |
-
"Option 1: Feature (ID, Locus Tag, or Name)\n"
|
| 202 |
-
"Example: 854068/YOL086C/ADH1 for S. cerevisiae alcohol dehydrogenase 1\n\n"
|
| 203 |
-
"Option 2: Position (chromosome,start,stop)\n"
|
| 204 |
-
"Example: 1,1,1000 for targeting chromosome 1, base pairs 1 to 1000\n\n"
|
| 205 |
-
"Option 3: Sequence (must be within the selected organism)\n"
|
| 206 |
-
"Example: Any nucleotide sequence between 100 and 10,000 base pairs.\n\n"
|
| 207 |
-
"*Note: to multiplex, separate multiple queries by new lines*\n"
|
| 208 |
-
"Example:\n"
|
| 209 |
-
"1,1,1000\n"
|
| 210 |
-
"5,1,500\n"
|
| 211 |
-
"etc.")
|
| 212 |
-
self.text_edit_gene_entry.setPlaceholderText(placeholder_text)
|
| 213 |
-
|
| 214 |
-
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 215 |
-
widget = self.findChild(widget_type, name)
|
| 216 |
-
if widget is None:
|
| 217 |
-
self.global_settings.logger.warning(f"Widget '{name}' not found in UI file.")
|
| 218 |
-
return widget
|
| 219 |
-
|
| 220 |
-
def _scale_ui(self) -> None:
|
| 221 |
-
scale_ui(self, custom_scale_width=1000, custom_scale_height=350)
|
| 222 |
-
|
| 223 |
-
def _handle_init_error(self, e: Exception) -> None:
|
| 224 |
-
error_msg = f"Error initializing MainWindowView: {str(e)}"
|
| 225 |
-
self.global_settings.logger.error(error_msg, exc_info=True)
|
| 226 |
-
show_error(self.global_settings, "Initialization Error", error_msg)
|
| 227 |
-
raise
|
| 228 |
-
|
| 229 |
-
def update_combo_box_endonuclease(self, endonuclease: list) -> None:
|
| 230 |
-
self.combo_box_endonuclease.clear()
|
| 231 |
-
self.combo_box_endonuclease.addItems(endonuclease)
|
| 232 |
-
|
| 233 |
-
def update_combo_box_organism(self, organisms: list) -> None:
|
| 234 |
-
self.combo_box_organism.clear()
|
| 235 |
-
self.combo_box_organism.addItems(organisms)
|
| 236 |
-
|
| 237 |
-
def update_combo_box_annotation_files(self, annotation_files: list) -> None:
|
| 238 |
-
self.combo_box_local_annotation_files.clear()
|
| 239 |
-
self.combo_box_local_annotation_files.addItems(annotation_files)
|
| 240 |
-
|
| 241 |
-
def set_progress_bar(self, value: int) -> None:
|
| 242 |
-
self.progress_bar_find_targets.setValue(value)
|
| 243 |
-
|
| 244 |
-
def reset_progress_bar(self) -> None:
|
| 245 |
-
self.set_progress_bar(0)
|
| 246 |
-
|
| 247 |
-
def update_theme_icon(self) -> None:
|
| 248 |
-
icon_name = "dark_mode.png" if self.global_settings.is_dark_mode() else "light_mode.png"
|
| 249 |
-
icon_path = os.path.join(self.global_settings.get_assets_dir(), icon_name)
|
| 250 |
-
icon = QIcon(icon_path)
|
| 251 |
-
self.theme_toggle_button.setIcon(icon)
|
| 252 |
-
self.theme_toggle_button.setIconSize(QtCore.QSize(16, 16)) # Reduced icon size from 18x18 to 16x16
|
| 253 |
-
|
| 254 |
-
def mousePressEvent(self, event):
|
| 255 |
-
if event.button() == Qt.MouseButton.LeftButton and self.title_bar.underMouse():
|
| 256 |
-
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
| 257 |
-
event.accept()
|
| 258 |
-
|
| 259 |
-
def mouseMoveEvent(self, event):
|
| 260 |
-
if event.buttons() & Qt.MouseButton.LeftButton and self.drag_position:
|
| 261 |
-
self.move(event.globalPosition().toPoint() - self.drag_position)
|
| 262 |
-
event.accept()
|
| 263 |
-
|
| 264 |
-
def mouseReleaseEvent(self, event):
|
| 265 |
-
self.drag_position = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from PyQt6.QtWidgets import (
|
| 2 |
QMainWindow, QPushButton, QWidget, QVBoxLayout,
|
| 3 |
-
QHBoxLayout, QLabel, QFrame, QMenu,
|
| 4 |
)
|
| 5 |
from PyQt6.QtGui import QIcon, QAction
|
| 6 |
from PyQt6.QtCore import Qt
|
|
@@ -17,7 +17,20 @@ class MainWindowView(QMainWindow, LoggingMixin):
|
|
| 17 |
QMainWindow.__init__(self)
|
| 18 |
LoggingMixin.__init__(self)
|
| 19 |
self.settings = global_settings
|
|
|
|
|
|
|
| 20 |
self.action_toggle_theme = QAction("Toggle Theme", self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
self._init_ui()
|
| 22 |
self.oldPos = None
|
| 23 |
|
|
@@ -47,26 +60,20 @@ class MainWindowView(QMainWindow, LoggingMixin):
|
|
| 47 |
self.log_debug(f"Window initialized at position ({x}, {y}) with size {final_size}")
|
| 48 |
|
| 49 |
def _init_window_properties(self) -> None:
|
| 50 |
-
|
| 51 |
-
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
| 52 |
-
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
| 53 |
-
|
| 54 |
toolbars = self.findChildren(QtWidgets.QToolBar)
|
| 55 |
for toolbar in toolbars:
|
| 56 |
toolbar.hide()
|
| 57 |
|
| 58 |
def _init_ui_elements(self) -> None:
|
| 59 |
self._init_menuBar()
|
| 60 |
-
self.
|
| 61 |
|
| 62 |
main_widget = QWidget()
|
| 63 |
main_layout = QVBoxLayout(main_widget)
|
| 64 |
main_layout.setContentsMargins(0, 0, 0, 0)
|
| 65 |
main_layout.setSpacing(0)
|
| 66 |
|
| 67 |
-
main_layout.addWidget(self.title_bar, 0)
|
| 68 |
-
main_layout.addWidget(self._init_divider(), 0)
|
| 69 |
-
|
| 70 |
# Create and set up tab container
|
| 71 |
tab_container = QWidget()
|
| 72 |
tab_container_layout = QVBoxLayout(tab_container)
|
|
@@ -93,11 +100,7 @@ class MainWindowView(QMainWindow, LoggingMixin):
|
|
| 93 |
self.setCentralWidget(main_widget)
|
| 94 |
|
| 95 |
def _init_menuBar(self) -> None:
|
| 96 |
-
|
| 97 |
-
self.action_open_genome_browser = self._find_widget("actOpenGenomeBrowser", QAction)
|
| 98 |
-
self.action_open_repository = self._find_widget("actionGoToCASPERRepository", QAction)
|
| 99 |
-
self.action_open_NCBI_BLAST = self._find_widget("actionGoToNCBIBLAST", QAction)
|
| 100 |
-
self.action_open_NCBI = self._find_widget("actGoToNCBI", QAction)
|
| 101 |
|
| 102 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 103 |
"""Find a widget by name and type"""
|
|
@@ -107,143 +110,8 @@ class MainWindowView(QMainWindow, LoggingMixin):
|
|
| 107 |
return widget
|
| 108 |
|
| 109 |
def _init_custom_title_bar(self) -> None:
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
self.title_bar.setFixedHeight(32)
|
| 113 |
-
|
| 114 |
-
# Create the main horizontal layout for the title bar
|
| 115 |
-
layout = QHBoxLayout(self.title_bar)
|
| 116 |
-
layout.setContentsMargins(10, 0, 10, 0)
|
| 117 |
-
layout.setSpacing(5)
|
| 118 |
-
|
| 119 |
-
# ----- Window Control Buttons -----
|
| 120 |
-
self.minimize_window_button = QPushButton("-", self.title_bar)
|
| 121 |
-
self.minimize_window_button.setObjectName("minimize_window_button")
|
| 122 |
-
self.minimize_window_button.setFixedSize(20, 20)
|
| 123 |
-
|
| 124 |
-
self.maximize_window_button = QPushButton("⛶", self.title_bar)
|
| 125 |
-
self.maximize_window_button.setObjectName("maximize_window_button")
|
| 126 |
-
self.maximize_window_button.setFixedSize(20, 20)
|
| 127 |
-
|
| 128 |
-
self.close_window_button = QPushButton("✕", self.title_bar)
|
| 129 |
-
self.close_window_button.setObjectName("close_window_button")
|
| 130 |
-
self.close_window_button.setFixedSize(20, 20)
|
| 131 |
-
|
| 132 |
-
button_style = """
|
| 133 |
-
QPushButton {
|
| 134 |
-
padding: 0px;
|
| 135 |
-
margin: 0px;
|
| 136 |
-
line-height: 20px;
|
| 137 |
-
text-align: center;
|
| 138 |
-
}
|
| 139 |
-
"""
|
| 140 |
-
self.minimize_window_button.setStyleSheet(button_style)
|
| 141 |
-
self.maximize_window_button.setStyleSheet(button_style)
|
| 142 |
-
self.close_window_button.setStyleSheet(button_style)
|
| 143 |
-
|
| 144 |
-
# ----- Left Widget (Minimize, Maximize, Close) -----
|
| 145 |
-
left_widget = QWidget()
|
| 146 |
-
left_layout = QHBoxLayout(left_widget)
|
| 147 |
-
left_layout.setContentsMargins(0, 0, 0, 0)
|
| 148 |
-
left_layout.setSpacing(5)
|
| 149 |
-
left_layout.addWidget(self.close_window_button)
|
| 150 |
-
left_layout.addWidget(self.minimize_window_button)
|
| 151 |
-
left_layout.addWidget(self.maximize_window_button)
|
| 152 |
-
|
| 153 |
-
# ----- Add Button with Dropdown -----
|
| 154 |
-
self.add_button = QPushButton(self.title_bar)
|
| 155 |
-
self.add_button.setObjectName("add_button")
|
| 156 |
-
self.add_button.setFixedSize(20, 20)
|
| 157 |
-
|
| 158 |
-
# Create the dropdown menu
|
| 159 |
-
self.add_menu = QMenu(self.add_button)
|
| 160 |
-
self.add_menu.setObjectName("add_menu")
|
| 161 |
-
|
| 162 |
-
# Add actions to the menu
|
| 163 |
-
self.action_new_genome = self.add_menu.addAction("New Genome")
|
| 164 |
-
self.action_new_endonuclease = self.add_menu.addAction("New Endonuclease")
|
| 165 |
-
|
| 166 |
-
# Set the menu for the button
|
| 167 |
-
self.add_button.setMenu(self.add_menu)
|
| 168 |
-
self.add_button.setStyleSheet("""
|
| 169 |
-
QPushButton {
|
| 170 |
-
padding: 0px;
|
| 171 |
-
margin: 0px;
|
| 172 |
-
text-align: center;
|
| 173 |
-
border: none;
|
| 174 |
-
}
|
| 175 |
-
QPushButton::menu-indicator {
|
| 176 |
-
width: 0px;
|
| 177 |
-
}
|
| 178 |
-
""")
|
| 179 |
-
|
| 180 |
-
# Initial icon will be set in update_plus_icon method
|
| 181 |
-
self.update_plus_icon()
|
| 182 |
-
|
| 183 |
-
# ----- Settings Button with Dropdown -----
|
| 184 |
-
self.settings_button = QPushButton(self.title_bar)
|
| 185 |
-
self.settings_button.setObjectName("settings_button")
|
| 186 |
-
self.settings_button.setFixedSize(20, 20)
|
| 187 |
-
|
| 188 |
-
# Create the settings dropdown menu
|
| 189 |
-
self.settings_menu = QMenu(self.settings_button)
|
| 190 |
-
self.settings_menu.setObjectName("settings_menu")
|
| 191 |
-
|
| 192 |
-
# Update the theme icon and add the action to the menu
|
| 193 |
-
self.update_theme_icon()
|
| 194 |
-
self.settings_menu.addAction(self.action_toggle_theme)
|
| 195 |
-
|
| 196 |
-
# Set the menu for the button
|
| 197 |
-
self.settings_button.setMenu(self.settings_menu)
|
| 198 |
-
self.settings_button.setStyleSheet("""
|
| 199 |
-
QPushButton {
|
| 200 |
-
padding: 0px;
|
| 201 |
-
margin: 0px;
|
| 202 |
-
text-align: center;
|
| 203 |
-
border: none;
|
| 204 |
-
}
|
| 205 |
-
QPushButton::menu-indicator {
|
| 206 |
-
width: 0px;
|
| 207 |
-
}
|
| 208 |
-
""")
|
| 209 |
-
|
| 210 |
-
# Initial icon will be set in update_settings_icon method
|
| 211 |
-
self.update_settings_icon()
|
| 212 |
-
|
| 213 |
-
# ----- Right Widget (Add Button + Settings Button + Stretch) -----
|
| 214 |
-
right_widget = QWidget()
|
| 215 |
-
right_layout = QHBoxLayout(right_widget)
|
| 216 |
-
right_layout.setContentsMargins(0, 0, 0, 0)
|
| 217 |
-
right_layout.setSpacing(5)
|
| 218 |
-
|
| 219 |
-
# Add a stretch to push the buttons to the far right within right_widget
|
| 220 |
-
right_layout.addStretch()
|
| 221 |
-
right_layout.addWidget(self.add_button)
|
| 222 |
-
right_layout.addWidget(self.settings_button)
|
| 223 |
-
|
| 224 |
-
# Adjust left_widget to calculate its required width
|
| 225 |
-
left_widget.adjustSize()
|
| 226 |
-
left_width = left_widget.sizeHint().width()
|
| 227 |
-
|
| 228 |
-
# Set right_widget's fixed width to match left_widget's width
|
| 229 |
-
right_widget.setFixedWidth(left_width)
|
| 230 |
-
|
| 231 |
-
self.title_label = QLabel("CASPER", self.title_bar)
|
| 232 |
-
self.title_label.setObjectName("title_label")
|
| 233 |
-
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the text in the label
|
| 234 |
-
|
| 235 |
-
# Add Widgets to the Main Title Bar Layout
|
| 236 |
-
layout.addWidget(left_widget)
|
| 237 |
-
layout.addStretch(1)
|
| 238 |
-
layout.addWidget(self.title_label)
|
| 239 |
-
layout.addStretch(1)
|
| 240 |
-
layout.addWidget(right_widget)
|
| 241 |
-
|
| 242 |
-
# Add mouse tracking to the title bar
|
| 243 |
-
self.title_bar.mousePressEvent = self.mousePressEvent
|
| 244 |
-
self.title_bar.mouseMoveEvent = self.mouseMoveEvent
|
| 245 |
-
self.title_bar.mouseReleaseEvent = self.mouseReleaseEvent
|
| 246 |
-
self.title_bar.setMouseTracking(True)
|
| 247 |
|
| 248 |
def _init_divider(self):
|
| 249 |
divider = QFrame()
|
|
@@ -352,31 +220,6 @@ class MainWindowView(QMainWindow, LoggingMixin):
|
|
| 352 |
QMenu {{ background-color: {theme['menu_bg_color']}; }}
|
| 353 |
QMenu::item:selected {{ background-color: {theme['menu_item_hover_bg_color']}; }}
|
| 354 |
QFrame#custom_divider {{ border-bottom: 1px solid {theme['divider_color']}; }}
|
| 355 |
-
|
| 356 |
-
QPushButton#add_button {{
|
| 357 |
-
background-color: {theme['button_bg_color']};
|
| 358 |
-
color: {theme['fg_color']};
|
| 359 |
-
border: 1px solid {theme['button_border_color']};
|
| 360 |
-
padding: 0px;
|
| 361 |
-
font-size: 16px;
|
| 362 |
-
line-height: 20px;
|
| 363 |
-
}}
|
| 364 |
-
|
| 365 |
-
QPushButton#add_button:hover {{
|
| 366 |
-
background-color: {theme['button_hover_bg_color']};
|
| 367 |
-
}}
|
| 368 |
-
|
| 369 |
-
QMenu {{
|
| 370 |
-
background-color: {theme['menu_bg_color']};
|
| 371 |
-
color: {theme['menu_text_color']};
|
| 372 |
-
border: 1px solid {theme['button_border_color']};
|
| 373 |
-
padding: 5px;
|
| 374 |
-
}}
|
| 375 |
-
|
| 376 |
-
QMenu::item:selected {{
|
| 377 |
-
background-color: {theme['menu_item_hover_bg_color']};
|
| 378 |
-
color: {theme['menu_hover_text_color']};
|
| 379 |
-
}}
|
| 380 |
""")
|
| 381 |
|
| 382 |
# Set the tab widget stylesheet
|
|
@@ -412,49 +255,8 @@ class MainWindowView(QMainWindow, LoggingMixin):
|
|
| 412 |
}}
|
| 413 |
""")
|
| 414 |
|
| 415 |
-
# Update
|
| 416 |
-
self.add_button.setStyleSheet(f"""
|
| 417 |
-
QPushButton {{
|
| 418 |
-
padding: 0px;
|
| 419 |
-
margin: 0px;
|
| 420 |
-
line-height: 0px;
|
| 421 |
-
text-align: center;
|
| 422 |
-
border: none;
|
| 423 |
-
font-size: 14px;
|
| 424 |
-
color: {theme['fg_color']};
|
| 425 |
-
}}
|
| 426 |
-
QPushButton:hover {{
|
| 427 |
-
background-color: {theme['button_hover_bg_color']};
|
| 428 |
-
}}
|
| 429 |
-
QPushButton::menu-indicator {{
|
| 430 |
-
width: 0px;
|
| 431 |
-
}}
|
| 432 |
-
""")
|
| 433 |
-
|
| 434 |
-
# Update the settings button styling
|
| 435 |
-
self.settings_button.setStyleSheet(f"""
|
| 436 |
-
QPushButton {{
|
| 437 |
-
padding: 0px;
|
| 438 |
-
margin: 0px;
|
| 439 |
-
line-height: 0px;
|
| 440 |
-
text-align: center;
|
| 441 |
-
border: none;
|
| 442 |
-
font-size: 14px;
|
| 443 |
-
color: {theme['fg_color']};
|
| 444 |
-
background-color: transparent;
|
| 445 |
-
}}
|
| 446 |
-
QPushButton:hover {{
|
| 447 |
-
background-color: {theme['button_hover_bg_color']};
|
| 448 |
-
}}
|
| 449 |
-
QPushButton::menu-indicator {{
|
| 450 |
-
width: 0px;
|
| 451 |
-
}}
|
| 452 |
-
""")
|
| 453 |
-
|
| 454 |
-
# Update icons
|
| 455 |
self.update_theme_icon()
|
| 456 |
-
self.update_plus_icon()
|
| 457 |
-
self.update_settings_icon()
|
| 458 |
|
| 459 |
def mousePressEvent(self, event):
|
| 460 |
"""Handle mouse press events for window dragging"""
|
|
@@ -471,4 +273,30 @@ class MainWindowView(QMainWindow, LoggingMixin):
|
|
| 471 |
def mouseReleaseEvent(self, event):
|
| 472 |
"""Handle mouse release events for window dragging"""
|
| 473 |
if event.button() == Qt.MouseButton.LeftButton:
|
| 474 |
-
self.oldPos = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from PyQt6.QtWidgets import (
|
| 2 |
QMainWindow, QPushButton, QWidget, QVBoxLayout,
|
| 3 |
+
QHBoxLayout, QLabel, QFrame, QMenu, QToolBar,
|
| 4 |
)
|
| 5 |
from PyQt6.QtGui import QIcon, QAction
|
| 6 |
from PyQt6.QtCore import Qt
|
|
|
|
| 17 |
QMainWindow.__init__(self)
|
| 18 |
LoggingMixin.__init__(self)
|
| 19 |
self.settings = global_settings
|
| 20 |
+
|
| 21 |
+
# Initialize all actions
|
| 22 |
self.action_toggle_theme = QAction("Toggle Theme", self)
|
| 23 |
+
self.action_new_genome = QAction("New Genome", self)
|
| 24 |
+
self.action_new_endonuclease = QAction("New Endonuclease", self)
|
| 25 |
+
self.action_change_database_directory = QAction("Change Database Directory", self)
|
| 26 |
+
self.action_open_repository = QAction("Open Repository", self)
|
| 27 |
+
self.action_open_NCBI = QAction("Open NCBI", self)
|
| 28 |
+
self.action_open_NCBI_BLAST = QAction("Open NCBI BLAST", self)
|
| 29 |
+
|
| 30 |
+
# Add keyboard shortcuts
|
| 31 |
+
self.action_new_genome.setShortcut("Ctrl+N") # Will be shown as Cmd+N on macOS
|
| 32 |
+
self.action_toggle_theme.setShortcut("Ctrl+T") # Will be shown as Cmd+T on macOS
|
| 33 |
+
|
| 34 |
self._init_ui()
|
| 35 |
self.oldPos = None
|
| 36 |
|
|
|
|
| 60 |
self.log_debug(f"Window initialized at position ({x}, {y}) with size {final_size}")
|
| 61 |
|
| 62 |
def _init_window_properties(self) -> None:
|
| 63 |
+
# Remove frameless window hint to show native window controls
|
|
|
|
|
|
|
|
|
|
| 64 |
toolbars = self.findChildren(QtWidgets.QToolBar)
|
| 65 |
for toolbar in toolbars:
|
| 66 |
toolbar.hide()
|
| 67 |
|
| 68 |
def _init_ui_elements(self) -> None:
|
| 69 |
self._init_menuBar()
|
| 70 |
+
self._setup_native_menu_bar()
|
| 71 |
|
| 72 |
main_widget = QWidget()
|
| 73 |
main_layout = QVBoxLayout(main_widget)
|
| 74 |
main_layout.setContentsMargins(0, 0, 0, 0)
|
| 75 |
main_layout.setSpacing(0)
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
# Create and set up tab container
|
| 78 |
tab_container = QWidget()
|
| 79 |
tab_container_layout = QVBoxLayout(tab_container)
|
|
|
|
| 100 |
self.setCentralWidget(main_widget)
|
| 101 |
|
| 102 |
def _init_menuBar(self) -> None:
|
| 103 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 106 |
"""Find a widget by name and type"""
|
|
|
|
| 110 |
return widget
|
| 111 |
|
| 112 |
def _init_custom_title_bar(self) -> None:
|
| 113 |
+
# Remove custom title bar implementation
|
| 114 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
def _init_divider(self):
|
| 117 |
divider = QFrame()
|
|
|
|
| 220 |
QMenu {{ background-color: {theme['menu_bg_color']}; }}
|
| 221 |
QMenu::item:selected {{ background-color: {theme['menu_item_hover_bg_color']}; }}
|
| 222 |
QFrame#custom_divider {{ border-bottom: 1px solid {theme['divider_color']}; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
""")
|
| 224 |
|
| 225 |
# Set the tab widget stylesheet
|
|
|
|
| 255 |
}}
|
| 256 |
""")
|
| 257 |
|
| 258 |
+
# Update theme icon
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
self.update_theme_icon()
|
|
|
|
|
|
|
| 260 |
|
| 261 |
def mousePressEvent(self, event):
|
| 262 |
"""Handle mouse press events for window dragging"""
|
|
|
|
| 273 |
def mouseReleaseEvent(self, event):
|
| 274 |
"""Handle mouse release events for window dragging"""
|
| 275 |
if event.button() == Qt.MouseButton.LeftButton:
|
| 276 |
+
self.oldPos = None
|
| 277 |
+
|
| 278 |
+
def _setup_native_menu_bar(self) -> None:
|
| 279 |
+
"""Setup the native menu bar for macOS"""
|
| 280 |
+
menubar = self.menuBar
|
| 281 |
+
|
| 282 |
+
# File Menu
|
| 283 |
+
file_menu = menubar.addMenu('File')
|
| 284 |
+
file_menu.addAction(self.action_change_database_directory)
|
| 285 |
+
|
| 286 |
+
# Add Menu (for New Genome and New Endonuclease)
|
| 287 |
+
add_menu = menubar.addMenu('Add')
|
| 288 |
+
add_menu.addAction(self.action_new_genome)
|
| 289 |
+
add_menu.addAction(self.action_new_endonuclease)
|
| 290 |
+
|
| 291 |
+
# Settings Menu
|
| 292 |
+
settings_menu = menubar.addMenu('Settings')
|
| 293 |
+
settings_menu.addAction(self.action_toggle_theme)
|
| 294 |
+
|
| 295 |
+
# Help Menu
|
| 296 |
+
help_menu = menubar.addMenu('Help')
|
| 297 |
+
help_menu.addAction(self.action_open_repository)
|
| 298 |
+
help_menu.addAction(self.action_open_NCBI)
|
| 299 |
+
help_menu.addAction(self.action_open_NCBI_BLAST)
|
| 300 |
+
|
| 301 |
+
# Make sure menu bar is visible
|
| 302 |
+
menubar.setNativeMenuBar(True) # Use native macOS menu bar
|
|
@@ -2,18 +2,24 @@ from typing import Optional
|
|
| 2 |
from PyQt6 import QtWidgets, uic, QtGui
|
| 3 |
from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
|
| 4 |
from PyQt6.QtCore import Qt
|
| 5 |
-
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
|
| 6 |
from matplotlib.figure import Figure
|
| 7 |
from matplotlib.ticker import MaxNLocator
|
| 8 |
from utils.ui import show_error
|
|
|
|
| 9 |
|
| 10 |
class MultitargetingWindowView(QtWidgets.QMainWindow):
|
| 11 |
def __init__(self, global_settings):
|
|
|
|
| 12 |
super().__init__()
|
| 13 |
self.settings = global_settings
|
| 14 |
self.logger = self.settings.get_logger()
|
| 15 |
|
|
|
|
| 16 |
self.init_ui()
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
def init_ui(self):
|
| 19 |
try:
|
|
@@ -134,26 +140,42 @@ class MultitargetingWindowView(QtWidgets.QMainWindow):
|
|
| 134 |
self.table_seeds.resizeColumnsToContents()
|
| 135 |
|
| 136 |
def setup_plots(self):
|
| 137 |
-
"""Initialize the matplotlib plots"""
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
(self
|
| 146 |
-
(self.
|
| 147 |
-
|
| 148 |
-
layout
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
def update_plots(self, repeats_data, sequences_data, chromosome_data):
|
| 153 |
"""Update all plots with new data"""
|
|
|
|
|
|
|
|
|
|
| 154 |
self._update_repeats_vs_seed_plot(repeats_data)
|
|
|
|
|
|
|
|
|
|
| 155 |
self._update_sequences_vs_repeats_plot(sequences_data)
|
|
|
|
|
|
|
|
|
|
| 156 |
self._update_repeat_vs_chromosome_plot(chromosome_data)
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
def _update_repeats_vs_seed_plot(self, data):
|
| 159 |
"""Update the repeats vs seed line plot"""
|
|
|
|
| 2 |
from PyQt6 import QtWidgets, uic, QtGui
|
| 3 |
from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
|
| 4 |
from PyQt6.QtCore import Qt
|
| 5 |
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
|
| 6 |
from matplotlib.figure import Figure
|
| 7 |
from matplotlib.ticker import MaxNLocator
|
| 8 |
from utils.ui import show_error
|
| 9 |
+
import time
|
| 10 |
|
| 11 |
class MultitargetingWindowView(QtWidgets.QMainWindow):
|
| 12 |
def __init__(self, global_settings):
|
| 13 |
+
start_time = time.time()
|
| 14 |
super().__init__()
|
| 15 |
self.settings = global_settings
|
| 16 |
self.logger = self.settings.get_logger()
|
| 17 |
|
| 18 |
+
init_ui_start = time.time()
|
| 19 |
self.init_ui()
|
| 20 |
+
self.logger.debug(f"UI initialization took: {time.time() - init_ui_start:.2f} seconds")
|
| 21 |
+
|
| 22 |
+
self.logger.debug(f"Total view initialization took: {time.time() - start_time:.2f} seconds")
|
| 23 |
|
| 24 |
def init_ui(self):
|
| 25 |
try:
|
|
|
|
| 140 |
self.table_seeds.resizeColumnsToContents()
|
| 141 |
|
| 142 |
def setup_plots(self):
|
| 143 |
+
"""Initialize the matplotlib plots only when needed"""
|
| 144 |
+
if not hasattr(self, 'repeats_vs_seed_canvas'):
|
| 145 |
+
self.repeats_vs_seed_canvas = MplCanvas(self, width=8, height=6)
|
| 146 |
+
layout = QtWidgets.QVBoxLayout(self.plot_repeats_vs_seed)
|
| 147 |
+
layout.setContentsMargins(0, 0, 0, 0)
|
| 148 |
+
layout.addWidget(self.repeats_vs_seed_canvas)
|
| 149 |
+
|
| 150 |
+
if not hasattr(self, 'sequences_vs_repeats_canvas'):
|
| 151 |
+
self.sequences_vs_repeats_canvas = MplCanvas(self, width=8, height=6)
|
| 152 |
+
layout = QtWidgets.QVBoxLayout(self.plot_sequences_vs_repeats)
|
| 153 |
+
layout.setContentsMargins(0, 0, 0, 0)
|
| 154 |
+
layout.addWidget(self.sequences_vs_repeats_canvas)
|
| 155 |
+
|
| 156 |
+
if not hasattr(self, 'repeat_vs_chromosome_canvas'):
|
| 157 |
+
self.repeat_vs_chromosome_canvas = MplCanvas(self, width=8, height=6)
|
| 158 |
+
layout = QtWidgets.QVBoxLayout(self.plot_repeat_vs_chromosome)
|
| 159 |
+
layout.setContentsMargins(0, 0, 0, 0)
|
| 160 |
+
layout.addWidget(self.repeat_vs_chromosome_canvas)
|
| 161 |
|
| 162 |
def update_plots(self, repeats_data, sequences_data, chromosome_data):
|
| 163 |
"""Update all plots with new data"""
|
| 164 |
+
start_time = time.time()
|
| 165 |
+
|
| 166 |
+
plot1_start = time.time()
|
| 167 |
self._update_repeats_vs_seed_plot(repeats_data)
|
| 168 |
+
self.logger.debug(f"Repeats vs seed plot update took: {time.time() - plot1_start:.2f} seconds")
|
| 169 |
+
|
| 170 |
+
plot2_start = time.time()
|
| 171 |
self._update_sequences_vs_repeats_plot(sequences_data)
|
| 172 |
+
self.logger.debug(f"Sequences vs repeats plot update took: {time.time() - plot2_start:.2f} seconds")
|
| 173 |
+
|
| 174 |
+
plot3_start = time.time()
|
| 175 |
self._update_repeat_vs_chromosome_plot(chromosome_data)
|
| 176 |
+
self.logger.debug(f"Chromosome plot update took: {time.time() - plot3_start:.2f} seconds")
|
| 177 |
+
|
| 178 |
+
self.logger.debug(f"Total plot updates took: {time.time() - start_time:.2f} seconds")
|
| 179 |
|
| 180 |
def _update_repeats_vs_seed_plot(self, data):
|
| 181 |
"""Update the repeats vs seed line plot"""
|
|
@@ -36,7 +36,6 @@ class NCBIWindowView(QtWidgets.QMainWindow):
|
|
| 36 |
self._init_ui_components()
|
| 37 |
|
| 38 |
self._is_initialized = True
|
| 39 |
-
self.logger.debug("NCBI Window initialization completed")
|
| 40 |
|
| 41 |
# Emit signal after everything is initialized
|
| 42 |
self.initialization_complete.emit()
|
|
@@ -61,9 +60,17 @@ class NCBIWindowView(QtWidgets.QMainWindow):
|
|
| 61 |
self.line_edit_strain = self._find_widget("ledStrain", QtWidgets.QLineEdit)
|
| 62 |
self.line_edit_max_results = self._find_widget("ledMaxResults", QtWidgets.QLineEdit)
|
| 63 |
self.check_box_complete_genomes_only = self._find_widget("chkCompleteGenomesOnly", QtWidgets.QCheckBox)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
# Set default values
|
| 66 |
self.line_edit_max_results.setText("100")
|
|
|
|
| 67 |
|
| 68 |
except Exception as e:
|
| 69 |
self.logger.error(f"Error initializing Step 1: {str(e)}")
|
|
@@ -151,7 +158,8 @@ class NCBIWindowView(QtWidgets.QMainWindow):
|
|
| 151 |
'refseq': self.radio_button_collections_refseq.isChecked(),
|
| 152 |
'genbank': self.radio_button_collections_genbank.isChecked(),
|
| 153 |
'fna': self.check_box_file_types_fna.isChecked(),
|
| 154 |
-
'gbff': self.check_box_file_types_gbff.isChecked()
|
|
|
|
| 155 |
}
|
| 156 |
|
| 157 |
def get_selected_rows(self):
|
|
|
|
| 36 |
self._init_ui_components()
|
| 37 |
|
| 38 |
self._is_initialized = True
|
|
|
|
| 39 |
|
| 40 |
# Emit signal after everything is initialized
|
| 41 |
self.initialization_complete.emit()
|
|
|
|
| 60 |
self.line_edit_strain = self._find_widget("ledStrain", QtWidgets.QLineEdit)
|
| 61 |
self.line_edit_max_results = self._find_widget("ledMaxResults", QtWidgets.QLineEdit)
|
| 62 |
self.check_box_complete_genomes_only = self._find_widget("chkCompleteGenomesOnly", QtWidgets.QCheckBox)
|
| 63 |
+
self.combo_box_database = self._find_widget("cmbDatabase", QtWidgets.QComboBox)
|
| 64 |
+
|
| 65 |
+
# Initialize database options
|
| 66 |
+
self.combo_box_database.addItems([
|
| 67 |
+
"NCBI GenBank",
|
| 68 |
+
"ENA (European Nucleotide Archive)"
|
| 69 |
+
])
|
| 70 |
|
| 71 |
# Set default values
|
| 72 |
self.line_edit_max_results.setText("100")
|
| 73 |
+
self.combo_box_database.setCurrentText("NCBI GenBank")
|
| 74 |
|
| 75 |
except Exception as e:
|
| 76 |
self.logger.error(f"Error initializing Step 1: {str(e)}")
|
|
|
|
| 158 |
'refseq': self.radio_button_collections_refseq.isChecked(),
|
| 159 |
'genbank': self.radio_button_collections_genbank.isChecked(),
|
| 160 |
'fna': self.check_box_file_types_fna.isChecked(),
|
| 161 |
+
'gbff': self.check_box_file_types_gbff.isChecked(),
|
| 162 |
+
'database': self.combo_box_database.currentText()
|
| 163 |
}
|
| 164 |
|
| 165 |
def get_selected_rows(self):
|
|
@@ -42,7 +42,6 @@ class StartupWindowView(QtWidgets.QMainWindow):
|
|
| 42 |
|
| 43 |
def _init_boxlayvBottom(self):
|
| 44 |
self.push_button_go_to_home_or_new_genome = self._find_widget('pbtnGoToHomeOrNewGenome', QtWidgets.QPushButton)
|
| 45 |
-
self.push_button_go_to_home_or_new_genome.clicked.connect(self._on_go_to_home_or_new_genome_clicked)
|
| 46 |
|
| 47 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 48 |
widget = self.findChild(widget_type, name)
|
|
@@ -81,8 +80,4 @@ class StartupWindowView(QtWidgets.QMainWindow):
|
|
| 81 |
self.label_db_status.show()
|
| 82 |
self.label_db_status.setStyleSheet("color: red;")
|
| 83 |
self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
|
| 84 |
-
self.push_button_go_to_home_or_new_genome.setEnabled(True)
|
| 85 |
-
|
| 86 |
-
def _on_go_to_home_or_new_genome_clicked(self):
|
| 87 |
-
if self.push_button_go_to_home_or_new_genome.text() == "Analyze a New Genome":
|
| 88 |
-
self.open_new_genome_requested.emit()
|
|
|
|
| 42 |
|
| 43 |
def _init_boxlayvBottom(self):
|
| 44 |
self.push_button_go_to_home_or_new_genome = self._find_widget('pbtnGoToHomeOrNewGenome', QtWidgets.QPushButton)
|
|
|
|
| 45 |
|
| 46 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 47 |
widget = self.findChild(widget_type, name)
|
|
|
|
| 80 |
self.label_db_status.show()
|
| 81 |
self.label_db_status.setStyleSheet("color: red;")
|
| 82 |
self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
|
| 83 |
+
self.push_button_go_to_home_or_new_genome.setEnabled(True)
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,11 +1,11 @@
|
|
| 1 |
from typing import Optional
|
| 2 |
-
from PyQt6 import QtWidgets, uic
|
| 3 |
from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
|
| 4 |
from PyQt6.QtGui import QTextDocument
|
| 5 |
from PyQt6.QtCore import Qt, pyqtSignal
|
| 6 |
from utils.ui import show_error
|
| 7 |
-
import time
|
| 8 |
import traceback
|
|
|
|
| 9 |
|
| 10 |
class ViewTargetsView(QtWidgets.QMainWindow):
|
| 11 |
# Define the signal
|
|
@@ -49,6 +49,9 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 49 |
self.table_guides.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
| 50 |
self.table_guides.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
| 51 |
|
|
|
|
|
|
|
|
|
|
| 52 |
# Enable horizontal scrolling
|
| 53 |
self.table_guides.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel)
|
| 54 |
self.table_guides.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
@@ -62,7 +65,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 62 |
# Set resize mode for header
|
| 63 |
header = self.table_guides.horizontalHeader()
|
| 64 |
header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.Interactive)
|
| 65 |
-
header.setStretchLastSection(False)
|
| 66 |
|
| 67 |
# Set minimum section size to prevent columns from becoming too narrow
|
| 68 |
header.setMinimumSectionSize(80)
|
|
@@ -79,9 +82,32 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 79 |
self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
|
| 80 |
self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
|
| 81 |
self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
|
|
|
|
| 82 |
|
| 83 |
self.text_edit_gene_viewer.setReadOnly(True)
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 86 |
widget = self.findChild(widget_type, name)
|
| 87 |
if widget is None:
|
|
@@ -89,9 +115,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 89 |
return widget
|
| 90 |
|
| 91 |
def display_guides_in_table(self, guides):
|
| 92 |
-
"""Ultra-fast guide display with virtual table and minimal UI updates"""
|
| 93 |
try:
|
| 94 |
-
# Store complete set of guides
|
| 95 |
self._all_guides = guides
|
| 96 |
|
| 97 |
selected_text = self.combo_box_gene.currentText()
|
|
@@ -161,14 +185,14 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 161 |
location = guide['location']
|
| 162 |
start_pos = location.split('-')[0] if '-' in location else location
|
| 163 |
|
| 164 |
-
# Create
|
| 165 |
items = [
|
| 166 |
-
(0,
|
| 167 |
(1, QTableWidgetItem(guide['endonuclease'])),
|
| 168 |
(2, QTableWidgetItem(guide['sequence'])),
|
| 169 |
(3, QTableWidgetItem(guide['strand'])),
|
| 170 |
(4, QTableWidgetItem(guide['pam'])),
|
| 171 |
-
(5,
|
| 172 |
(6, QTableWidgetItem("--.--")) # Off-target placeholder
|
| 173 |
]
|
| 174 |
|
|
@@ -185,7 +209,8 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 185 |
|
| 186 |
# Add Azimuth score if column exists
|
| 187 |
if azimuth_index is not None and 'azimuth_score' in guide:
|
| 188 |
-
|
|
|
|
| 189 |
self.table_guides.setItem(row, azimuth_index, azimuth_item)
|
| 190 |
|
| 191 |
# Updated column widths
|
|
@@ -222,6 +247,25 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 222 |
self.logger.error(f"Error in display_guides: {str(e)}")
|
| 223 |
show_error(self.settings, "Error displaying guides", str(e))
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
def _handle_scroll_virtual(self, value, total_rows, row_height, buffer_rows):
|
| 226 |
try:
|
| 227 |
if not hasattr(self, '_all_guides') or not self._all_guides:
|
|
@@ -242,16 +286,27 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 242 |
if row < len(self._all_guides) and not self.table_guides.item(row, 0):
|
| 243 |
guide = self._all_guides[row]
|
| 244 |
|
| 245 |
-
#
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
| 253 |
self.table_guides.setItem(row, col, item)
|
| 254 |
|
|
|
|
| 255 |
if not self.table_guides.cellWidget(row, 7):
|
| 256 |
details_button = QtWidgets.QPushButton("Details")
|
| 257 |
self.table_guides.setCellWidget(row, 7, details_button)
|
|
@@ -328,7 +383,6 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 328 |
|
| 329 |
def set_combo_box_gene(self, genes):
|
| 330 |
try:
|
| 331 |
-
|
| 332 |
# Disable UI updates
|
| 333 |
self.combo_box_gene.blockSignals(True)
|
| 334 |
self.combo_box_gene.setUpdatesEnabled(False)
|
|
@@ -339,16 +393,19 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 339 |
# Debug logging
|
| 340 |
self.logger.debug(f"Received {len(genes)} genes")
|
| 341 |
|
|
|
|
|
|
|
|
|
|
| 342 |
# Add items in a single batch
|
| 343 |
-
if
|
| 344 |
# Pre-allocate size
|
| 345 |
-
self.combo_box_gene.insertItems(0,
|
| 346 |
|
| 347 |
# Set first item without triggering updates
|
| 348 |
if self.combo_box_gene.count() > 0:
|
| 349 |
self.combo_box_gene.setCurrentIndex(0)
|
| 350 |
|
| 351 |
-
self.logger.debug(f"Added {len(
|
| 352 |
|
| 353 |
# Re-enable UI updates
|
| 354 |
self.combo_box_gene.setUpdatesEnabled(True)
|
|
@@ -370,15 +427,24 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 370 |
except Exception as e:
|
| 371 |
self.logger.error(f"Error setting gene viewer text: {str(e)}")
|
| 372 |
|
| 373 |
-
def
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
def update_gene_viewer(self, sequence):
|
| 378 |
self.text_edit_gene_viewer.clear()
|
| 379 |
doc = QTextDocument()
|
| 380 |
doc.setHtml(sequence)
|
| 381 |
self.text_edit_gene_viewer.setDocument(doc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
def select_all_guides(self, select):
|
| 384 |
for row in range(self.table_guides.rowCount()):
|
|
@@ -491,4 +557,42 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 491 |
|
| 492 |
except Exception as e:
|
| 493 |
self.logger.error(f"Error showing details: {str(e)}")
|
| 494 |
-
show_error(self.settings, "Error showing details", str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from typing import Optional
|
| 2 |
+
from PyQt6 import QtWidgets, uic
|
| 3 |
from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
|
| 4 |
from PyQt6.QtGui import QTextDocument
|
| 5 |
from PyQt6.QtCore import Qt, pyqtSignal
|
| 6 |
from utils.ui import show_error
|
|
|
|
| 7 |
import traceback
|
| 8 |
+
from views.DNAFeatureViewer import DNAFeatureViewer
|
| 9 |
|
| 10 |
class ViewTargetsView(QtWidgets.QMainWindow):
|
| 11 |
# Define the signal
|
|
|
|
| 49 |
self.table_guides.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
| 50 |
self.table_guides.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
| 51 |
|
| 52 |
+
# Enable sorting
|
| 53 |
+
self.table_guides.setSortingEnabled(True)
|
| 54 |
+
|
| 55 |
# Enable horizontal scrolling
|
| 56 |
self.table_guides.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel)
|
| 57 |
self.table_guides.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
|
|
| 65 |
# Set resize mode for header
|
| 66 |
header = self.table_guides.horizontalHeader()
|
| 67 |
header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.Interactive)
|
| 68 |
+
header.setStretchLastSection(False)
|
| 69 |
|
| 70 |
# Set minimum section size to prevent columns from becoming too narrow
|
| 71 |
header.setMinimumSectionSize(80)
|
|
|
|
| 82 |
self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
|
| 83 |
self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
|
| 84 |
self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
|
| 85 |
+
self.check_box_view_exons_only = self._find_widget('chkViewExonsOnly', QtWidgets.QCheckBox)
|
| 86 |
|
| 87 |
self.text_edit_gene_viewer.setReadOnly(True)
|
| 88 |
|
| 89 |
+
# Create DNA feature viewer
|
| 90 |
+
self.dna_feature_viewer = DNAFeatureViewer()
|
| 91 |
+
|
| 92 |
+
# Get the layout of the gene viewer group
|
| 93 |
+
gene_viewer_group = self.findChild(QtWidgets.QGroupBox, 'grpGeneViewer')
|
| 94 |
+
gene_viewer_layout = gene_viewer_group.layout()
|
| 95 |
+
|
| 96 |
+
# Find the row index of the text editor
|
| 97 |
+
text_editor_row = -1
|
| 98 |
+
for i in range(gene_viewer_layout.rowCount()):
|
| 99 |
+
item = gene_viewer_layout.itemAtPosition(i, 0)
|
| 100 |
+
if item and item.widget() == self.text_edit_gene_viewer:
|
| 101 |
+
text_editor_row = i
|
| 102 |
+
break
|
| 103 |
+
|
| 104 |
+
if text_editor_row != -1:
|
| 105 |
+
# Insert DNA feature viewer above the text editor
|
| 106 |
+
gene_viewer_layout.addWidget(self.dna_feature_viewer, text_editor_row, 0, 1, -1)
|
| 107 |
+
|
| 108 |
+
# Connect signals
|
| 109 |
+
self.dna_feature_viewer.sequence_selected.connect(self._on_sequence_selected)
|
| 110 |
+
|
| 111 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 112 |
widget = self.findChild(widget_type, name)
|
| 113 |
if widget is None:
|
|
|
|
| 115 |
return widget
|
| 116 |
|
| 117 |
def display_guides_in_table(self, guides):
|
|
|
|
| 118 |
try:
|
|
|
|
| 119 |
self._all_guides = guides
|
| 120 |
|
| 121 |
selected_text = self.combo_box_gene.currentText()
|
|
|
|
| 185 |
location = guide['location']
|
| 186 |
start_pos = location.split('-')[0] if '-' in location else location
|
| 187 |
|
| 188 |
+
# Create items with proper data roles for sorting
|
| 189 |
items = [
|
| 190 |
+
(0, self._create_sortable_item(start_pos, int(start_pos))), # Location as number
|
| 191 |
(1, QTableWidgetItem(guide['endonuclease'])),
|
| 192 |
(2, QTableWidgetItem(guide['sequence'])),
|
| 193 |
(3, QTableWidgetItem(guide['strand'])),
|
| 194 |
(4, QTableWidgetItem(guide['pam'])),
|
| 195 |
+
(5, self._create_sortable_item(str(guide['score']), float(guide['score']))), # Score as number
|
| 196 |
(6, QTableWidgetItem("--.--")) # Off-target placeholder
|
| 197 |
]
|
| 198 |
|
|
|
|
| 209 |
|
| 210 |
# Add Azimuth score if column exists
|
| 211 |
if azimuth_index is not None and 'azimuth_score' in guide:
|
| 212 |
+
azimuth_score = float(guide['azimuth_score'])
|
| 213 |
+
azimuth_item = self._create_sortable_item(str(azimuth_score), azimuth_score)
|
| 214 |
self.table_guides.setItem(row, azimuth_index, azimuth_item)
|
| 215 |
|
| 216 |
# Updated column widths
|
|
|
|
| 247 |
self.logger.error(f"Error in display_guides: {str(e)}")
|
| 248 |
show_error(self.settings, "Error displaying guides", str(e))
|
| 249 |
|
| 250 |
+
def _create_sortable_item(self, display_text, sort_value):
|
| 251 |
+
"""Create a table item that displays text but sorts by numeric value"""
|
| 252 |
+
item = QTableWidgetItem()
|
| 253 |
+
item.setData(Qt.ItemDataRole.DisplayRole, sort_value) # Use raw value for display
|
| 254 |
+
item.setData(Qt.ItemDataRole.EditRole, sort_value) # Used for sorting
|
| 255 |
+
|
| 256 |
+
# Format display text based on value type
|
| 257 |
+
if isinstance(sort_value, (int, float)):
|
| 258 |
+
if isinstance(sort_value, int):
|
| 259 |
+
# For integers (like positions), show full number
|
| 260 |
+
item.setText(f"{sort_value:d}")
|
| 261 |
+
else:
|
| 262 |
+
# For floats (like scores), show with 2 decimal places
|
| 263 |
+
item.setText(f"{sort_value:.2f}")
|
| 264 |
+
else:
|
| 265 |
+
item.setText(str(sort_value))
|
| 266 |
+
|
| 267 |
+
return item
|
| 268 |
+
|
| 269 |
def _handle_scroll_virtual(self, value, total_rows, row_height, buffer_rows):
|
| 270 |
try:
|
| 271 |
if not hasattr(self, '_all_guides') or not self._all_guides:
|
|
|
|
| 286 |
if row < len(self._all_guides) and not self.table_guides.item(row, 0):
|
| 287 |
guide = self._all_guides[row]
|
| 288 |
|
| 289 |
+
# Extract start position from location
|
| 290 |
+
location = guide['location']
|
| 291 |
+
start_pos = location.split('-')[0] if '-' in location else location
|
| 292 |
+
|
| 293 |
+
# Create items with proper data roles for sorting
|
| 294 |
+
items = [
|
| 295 |
+
(0, self._create_sortable_item(start_pos, int(start_pos))), # Location as number
|
| 296 |
+
(1, QTableWidgetItem(guide['endonuclease'])),
|
| 297 |
+
(2, QTableWidgetItem(guide['sequence'])),
|
| 298 |
+
(3, QTableWidgetItem(guide['strand'])),
|
| 299 |
+
(4, QTableWidgetItem(guide['pam'])),
|
| 300 |
+
(5, self._create_sortable_item(str(guide['score']), float(guide['score']))), # Score as number
|
| 301 |
+
(6, QTableWidgetItem("--.--")) # Off-target placeholder
|
| 302 |
+
]
|
| 303 |
+
|
| 304 |
+
# Set items with flags
|
| 305 |
+
for col, item in items:
|
| 306 |
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
| 307 |
self.table_guides.setItem(row, col, item)
|
| 308 |
|
| 309 |
+
# Add details button
|
| 310 |
if not self.table_guides.cellWidget(row, 7):
|
| 311 |
details_button = QtWidgets.QPushButton("Details")
|
| 312 |
self.table_guides.setCellWidget(row, 7, details_button)
|
|
|
|
| 383 |
|
| 384 |
def set_combo_box_gene(self, genes):
|
| 385 |
try:
|
|
|
|
| 386 |
# Disable UI updates
|
| 387 |
self.combo_box_gene.blockSignals(True)
|
| 388 |
self.combo_box_gene.setUpdatesEnabled(False)
|
|
|
|
| 393 |
# Debug logging
|
| 394 |
self.logger.debug(f"Received {len(genes)} genes")
|
| 395 |
|
| 396 |
+
# Use a set to ensure uniqueness
|
| 397 |
+
unique_genes = list(set(genes))
|
| 398 |
+
|
| 399 |
# Add items in a single batch
|
| 400 |
+
if unique_genes:
|
| 401 |
# Pre-allocate size
|
| 402 |
+
self.combo_box_gene.insertItems(0, unique_genes)
|
| 403 |
|
| 404 |
# Set first item without triggering updates
|
| 405 |
if self.combo_box_gene.count() > 0:
|
| 406 |
self.combo_box_gene.setCurrentIndex(0)
|
| 407 |
|
| 408 |
+
self.logger.debug(f"Added {len(unique_genes)} unique genes to combo box")
|
| 409 |
|
| 410 |
# Re-enable UI updates
|
| 411 |
self.combo_box_gene.setUpdatesEnabled(True)
|
|
|
|
| 427 |
except Exception as e:
|
| 428 |
self.logger.error(f"Error setting gene viewer text: {str(e)}")
|
| 429 |
|
| 430 |
+
def update_gene_viewer(self, sequence, features=None):
|
| 431 |
+
"""Update both text editor and DNA feature viewer"""
|
| 432 |
+
# Update text editor
|
|
|
|
|
|
|
| 433 |
self.text_edit_gene_viewer.clear()
|
| 434 |
doc = QTextDocument()
|
| 435 |
doc.setHtml(sequence)
|
| 436 |
self.text_edit_gene_viewer.setDocument(doc)
|
| 437 |
+
|
| 438 |
+
# Get start position from line edit
|
| 439 |
+
try:
|
| 440 |
+
start_pos = int(self.line_edit_start_location.text())
|
| 441 |
+
except (ValueError, TypeError):
|
| 442 |
+
start_pos = 1
|
| 443 |
+
|
| 444 |
+
# Update DNA feature viewer
|
| 445 |
+
if features is None:
|
| 446 |
+
features = []
|
| 447 |
+
self.dna_feature_viewer.set_data(sequence, features, start_pos)
|
| 448 |
|
| 449 |
def select_all_guides(self, select):
|
| 450 |
for row in range(self.table_guides.rowCount()):
|
|
|
|
| 557 |
|
| 558 |
except Exception as e:
|
| 559 |
self.logger.error(f"Error showing details: {str(e)}")
|
| 560 |
+
show_error(self.settings, "Error showing details", str(e))
|
| 561 |
+
|
| 562 |
+
def _on_sequence_selected(self, start, end):
|
| 563 |
+
"""Handle sequence selection in DNA feature viewer"""
|
| 564 |
+
self.line_edit_start_location.setText(str(start))
|
| 565 |
+
self.line_edit_stop_location.setText(str(end))
|
| 566 |
+
|
| 567 |
+
def highlight_guides_in_viewer(self, guides_to_highlight, sequence):
|
| 568 |
+
"""Highlight guides in viewer"""
|
| 569 |
+
try:
|
| 570 |
+
for guide in guides_to_highlight:
|
| 571 |
+
sequence_to_find = guide['sequence']
|
| 572 |
+
strand = guide['strand']
|
| 573 |
+
|
| 574 |
+
if strand == '-':
|
| 575 |
+
sequence_to_find = str(Seq(sequence_to_find).reverse_complement())
|
| 576 |
+
|
| 577 |
+
sequence_upper = sequence.upper()
|
| 578 |
+
target_upper = sequence_to_find.upper()
|
| 579 |
+
|
| 580 |
+
pos = sequence_upper.find(target_upper)
|
| 581 |
+
if pos != -1:
|
| 582 |
+
# Set color based on strand
|
| 583 |
+
color = QColor(255, 0, 0, 100) if strand == '-' else QColor(0, 255, 0, 100)
|
| 584 |
+
|
| 585 |
+
# Highlight sequence in viewer
|
| 586 |
+
self.dna_feature_viewer.sequence_viewer.highlight_sequence(
|
| 587 |
+
pos,
|
| 588 |
+
pos + len(sequence_to_find) - 1,
|
| 589 |
+
color
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
except Exception as e:
|
| 593 |
+
self.logger.error(f"Error highlighting guides: {str(e)}")
|
| 594 |
+
show_error(self.settings, "Error highlighting guides", str(e))
|
| 595 |
+
|
| 596 |
+
def clear_highlights(self):
|
| 597 |
+
"""Clear highlights in viewer"""
|
| 598 |
+
self.dna_feature_viewer.sequence_viewer.clear_highlights()
|